Skip to content

一、 同步

1.1 synchronized关键字

synchronized锁 可能锁对象包括: this, 临界资源对象,Class类对象。

synchronized保证原子性的原理,synchronized保证只有一个线程拿到锁,能够进入同步代码块

synchronized保证可见性的原理,执行synchronized时,会对应lock原子操作会刷新工作内存中共享变量的值

特性:

可重入 一个线程可以多次执行synchronized,重复获取同一把锁。原理:synchronized的锁对象中有一个计数器(recursions变量)会记录线程获得几次锁.

不可中断 :一个线程获得锁后,另一个线程想要获得锁,必须处于阻塞或等待状态,如果第一个线程不释放锁,第二个线程会一直阻塞或等待,不可被中断。

不可中断是指,当一个线程获得锁后,另一个线程一直处于阻塞或等待状态,前一个线程不释放锁,后一个线程会一直阻塞或等待,不可被中断。synchronized属于不可被中断 Lock的lock方法是不可中断的 Lock的tryLock方法是可中断的

1.1.1 同步方法

synchronized T methodName(){} 同步方法锁定的是当前对象。当多线程通过同一个对象引用多次调用当前同步方法时,需同步执行

1.1.2 同步代码块

同步代码块的同步粒度更加细致,是商业开发中推荐的编程方式。可以定位到具体的同步位置,而不是简单的将方法整体实现同步逻辑。在效率上,相对更高。

1.1.2.1 锁定临界对象

T methodName(){ synchronized(object){} } 同步代码块在执行时,是锁定object对象。当多个线程调用同一个方法时,锁定对象不变的情况下,需同步执行。

1.1.2.2 锁定当前对象

T methodName(){ synchronized(this){} } 当锁定对象为this时,相当于1.1同步方法。

1.1.3 锁的底层实现

Java 虚拟机中的同步(Synchronization)基于进入和退出管程(Monitor)对象实现。同步方法 并不是由 monitor enter 和 monitor exit 指令来实现同步的,而是由方法调用指令读取运行时常量池中方法的 ACC_SYNCHRONIZED 标志来隐式实现的。

1.1.3.1 对象内存简图

image-20210124114347488

对象头:存储对象的hashCode、锁信息或分代年龄或GC标志,类型指针指向对象的类元数据,JVM通过这个指针确定该对象是哪个类的实例等信息。

实例变量:存放类的属性数据信息,包括父类的属性信息

填充数据:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐

当在对象上加锁时,数据是记录在对象头中。当执行synchronized同步方法或同步代码块时,会在对象头中记录锁标记,锁标记指向的是monitor对象(也称为管程或监视器锁)的起始地址。每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。

在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的。

ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,以及_Owner标记。其中_WaitSet是用于管理等待队列(wait)线程的,_EntryList是用于管理锁池阻塞线程的,_Owner标记用于记录当前执行线程。线程状态图如下:

image-20210124114424255

当多线程并发访问同一个同步代码时,首先会进入_EntryList,当线程获取锁标记后,monitor 中的_Owner 记录此线程,并在monitor 中的计数器执行递增计算(+1),代表锁定,其他线程在_EntryList 中继续阻塞。若执行线程调用wait 方法,则monitor 中的计数器执行赋值为0 计算,并将_Owner 标记赋值为null,代表放弃锁,执行线程进如_WaitSet 中阻塞。若执行线程调用notify/notifyAll 方法,_WaitSet 中的线程被唤醒,进入_EntryList 中阻塞,等待获取锁标记。若执行线程的同步代码执行结束,同样会释放锁标记,monitor 中的_Owner标记赋值为null,且计数器赋值为0 计算。

1.1.4 锁的种类

Java 中锁的种类大致分为偏向锁,自旋锁,轻量级锁,重量级锁。

锁的使用方式为:先提供偏向锁,如果不满足的时候,升级为轻量级锁,再不满足,升级为重量级锁。自旋锁是一个过渡的锁状态,不是一种实际的锁类型。

锁只能升级,不能降级。

1.1.4.1 重量级锁

在1.1.3 中解释的就是重量级锁。

1.1.4.2 偏向锁

偏向锁是JDK 6中的重要引进

偏向锁的“偏”,就是偏心的“偏”、偏袒的“偏”,它的意思是这个锁会偏向于第一个获得它的线程,会在对象头存储锁偏向的线程ID,以后该线程进入和退出同步块时只需要检查是否为偏向锁、锁标志位以及ThreadID即可。不过一旦出现多个线程竞争时必须撤销偏向锁,所以撤销偏向锁消耗的性能必须小于之前节省下来的CAS原子操作的性能消耗,不然就得不偿失了。

是一种编译解释锁。如果代码中不可能出现多线程并发争抢同一个锁的时候,JVM 编译代码,解释执行的时候,会自动的放弃同步信息。消除synchronized 的同步代码结果。使用锁标记的形式记录锁状态。在Monitor 中有变量ACC_SYNCHRONIZED。当变量值使用的时候,代表偏向锁锁定。可以避免锁的争抢和锁池状态的维护。提高效率。

原理:

当线程第一次访问同步块并获取锁时,偏向锁处理流程如下:

  1. 虚拟机将会把对象头中的标志位设为“01”,即偏向模式。
  2. 同时使用CAS操作把获取到这个锁的线程的ID记录在对象的Mark Word之中 ,如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作,偏向锁的效率高。

持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作,偏向锁的效率高

偏向锁的撤销

  1. 偏向锁的撤销动作必须等待全局安全点
  2. 暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态
  3. 撤销偏向锁,恢复到无锁(标志位为 01)或轻量级锁(标志位为 00)的状态

偏向锁在Java 6之后是默认启用的,但在应用程序启动几秒钟之后才激活,可以使用 - XX:BiasedLockingStartupDelay=0 参数关闭延迟,如果确定应用程序中所有锁通常情况下处于竞争状态,可以通过 XX:-UseBiasedLocking=false 参数关闭偏向锁。

偏向锁好处

偏向锁是在只有一个线程执行同步块时进一步提高性能,适用于一个线程反复获得同一锁的情况。偏向锁可以提高带有同步但无竞争的程序性能。

它同样是一个带有效益权衡性质的优化,也就是说,它并不一定总是对程序运行有利,如果程序中大多数的锁总是被多个不同的线程访问比如线程池,那偏向模式就是多余的。

在JDK5中偏向锁默认是关闭的,而到了JDK6中偏向锁已经默认开启。但在应用程序启动几秒钟之后才激活,可以使用 -XX:BiasedLockingStartupDelay=0 参数关闭延迟,如果确定应用程序中所有锁通常情况下处于竞争状态,可以通过 XX:-UseBiasedLocking=false 参数关闭偏向锁。

1.1.4.3 轻量级锁

轻量级锁是JDK 6之中加入的新型锁机制,它名字中的“轻量级”是相对于使用monitor的传统锁而言的,因此传统的锁机制就称为“重量级”锁。

引入轻量级锁的目的:在多线程交替执行同步块的情况下,尽量避免重量级锁引起的性能消耗,但是如果多个线程在同一时刻进入临界区,会导致轻量级锁膨胀升级重量级锁,所以轻量级锁的出现并非是要替代重量级锁。

过渡锁。当偏向锁不满足,也就是有多线程并发访问,锁定同一个对象的时候,先提升为轻量级锁。也是使用标记ACC_SYNCHRONIZED标记记录的。ACC_UNSYNCHRONIZED标记记录未获取到锁信息的线程。就是只有两个线程争抢锁标记的时候,优先使用轻量级锁。

两个线程也可能出现重量级锁。

原理:

当关闭偏向锁功能或者多个线程竞争偏向锁导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁,其步骤如下: 获取锁

  1. 判断当前对象是否处于无锁状态(hashcode、0、01),如果是,则JVM首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝(官方把这份拷贝加了一个Displaced前缀,即Displaced Mark Word),将对象的Mark Word复制到栈帧中的Lock Record中,将Lock Reocrd中的owner指向当前对象。
  2. JVM利用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,如果成功表示竞争到锁,则将锁标志位变成00,执行同步操作。
  3. 如果失败则判断当前对象的Mark Word是否指向当前线程的栈帧,如果是则表示当前线程已经持有当前对象的锁,则直接执行同步代码块;否则只能说明该锁对象已经被其他线程抢占了,这时轻量级锁需要膨胀为重量级锁,锁标志位变成10,后面等待的线程将会进入阻塞状态。

轻量级锁的释放

轻量级锁的释放也是通过CAS操作来进行的,主要步骤如下:

  1. 取出在获取轻量级锁保存在Displaced Mark Word中的数据。
  2. 用CAS操作将取出的数据替换当前对象的Mark Word中,如果成功,则说明释放锁成功
  3. 如果CAS操作替换失败,说明有其他线程尝试获取该锁,则需要将轻量级锁需要膨胀升级为重量级锁。

对于轻量级锁,其性能提升的依据是“对于绝大部分的锁,在整个生命周期内都是不会存在竞争的”,如果打破这个依据则除了互斥的开销外,还有额外的CAS操作,因此在有多线程竞争的情况下,轻量级锁比重量级锁更慢。

轻量级锁好处

在多线程交替执行同步块的情况下,可以避免重量级锁引起的性能消耗。

1.1.4.4 自旋锁

让后面请求锁的那个线程“稍等一下”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只需让线程执行一个忙循环(自 旋) , 这项技术就是所谓的自旋锁。

自旋锁在JDK 1.4.2中就已经引入 ,只不过默认是关闭的,可以使用-XX:+UseSpinning参数来开启,在JDK 6中 就已经改为默认开启了。自旋等待不能代替阻塞,且先不说对处理器数量的要求,自旋等待本身虽然避免了线程切换的开销,但它是要占用处理器时间的,因此,如果锁被占用的时间很短,自旋等待的效果就会非常好,反之,如果锁被占用的时间很长。那么自旋的线程只会白白消耗处理器资源,而不会做任何有用的工作,反而会带来性 能上的浪费。因此,自旋等待的时间必须要有一定的限度,如果自旋超过了限定的次数仍然没有成功获得锁,就应当使用传统的方式去挂起线程了。自旋次数的默认值是10次,用户可以使用参数-XX : PreBlockSpin来更改。

是一个过渡锁,是偏向锁和轻量级锁的过渡。

当获取锁的过程中,未获取到。为了提高效率,JVM自动执行若干次空循环,再次申请锁,而不是进入阻塞状态的情况。称为自旋锁。自旋锁提高效率就是避免线程状态的变更。

适应性自旋锁

在JDK 6中引入了自适应的自旋锁。自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间,比如100次循环。另外,如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。有了自适应自旋,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测就会越来越准确,虛拟机就会变得越来越“聪明”了。

1.1.5 锁消除

锁消除是指虚拟机即时编译器(JIT)在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判定依据来源于逃逸分析的数据支持,如果判断在一段代码中,堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把它们当做栈上数据对待,认为它们是线程私有的,同步加锁自然就无须进行。变量是否逃逸,对于虚拟机来说需要使用数据流分析来确定,但是程序员自己应该是很清楚的,怎么会在明知道不存在数据争用的情况下要求同步呢?实际上有许多同步措施并不是程序员自己加入的,同步的代码在Java程序中的普遍程度也许超过了大部分读者的想象

StringBuffer的append ( ) 是一个同步方法,锁就是this也就是(new StringBuilder())。虚拟机发现它的动态作用域被限制在concatString( )方法内部。也就是说, new StringBuilder()对象的引用永远不会“逃 逸”到concatString ( )方法之外,其他线程无法访问到它,因此,虽然这里有锁,但是可以被安全地消除掉,在即时编译之后,这段代码就会忽略掉所有的同步而直接执行了。

1.1.6 锁粗化

原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小,只在共享数据的实际作用域中才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待锁的线程也能尽快拿到锁。大部分情况下,上面的原则都是正确的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。

1.1.7 synchronized原理

javap 反汇编

synchronized是一个关键字,看不到源码。我们可以将class文件进行反汇编。

sh
# JDK自带的一个工具: javap ,对字节码进行反汇编,查看字节码指令。


<NolebasePageProperties />




javap -p -v -c C:\Users\13666\IdeaProjects\HeiMa\Synchronized\target\classes\com\itheima\demo04 _synchronized_monitor\Increment.class

monitorenter

每一个对象都会和一个监视器monitor关联。监视器被占用时会被锁住,其他线程无法来获取该monitor。 当JVM执行某个线程的某个方法内部的monitorenter时,它会尝试去获取当前对象对应的monitor的所有权。其过程如下:

  1. 若monior的进入数为0,线程可以进入monitor,并将monitor的进入数置为1。当前线程成为monitor的owner(所有者)
  2. 若线程已拥有monitor的所有权,允许它重入monitor,则进入monitor的进入数加1
  3. 若其他线程已经占有monitor的所有权,那么当前尝试获monitor的所有权的线程会被阻塞,直到monitor的进入数变为0,才能重新尝试获取monitor的所有权。

synchronized的锁对象会关联一个monitor,这个monitor不是我们主动创建的,是JVM的线程执行到这个同步代码块,发现锁对象没有monitor就会创建monitor,monitor内部有两个重要的成员变量owner:拥有这把锁的线程,recursions会记录线程拥有锁的次数,当一个线程拥有monitor后其他线程只能等待

monitorexit

  1. 能执行monitorexit指令的线程一定是拥有当前对象的monitor的所有权的线程。
  2. 执行monitorexit时会将monitor的进入数减1。当monitor的进入数减为0时,当前线程退出monitor,不再拥有monitor的所有权,此时其他被这个monitor阻塞的线程可以尝试去获取这个monitor的所有权

monitorexit释放锁。

monitorexit插入在方法结束处和异常处,JVM保证每个monitorenter必须有对应的monitorexit。

同步方法

同步方法在反汇编后,会增加 ACC_SYNCHRONIZED 修饰。会隐式调用monitorenter和monitorexit。在执行同步方法前会调用monitorenter,在执行完同步方法后会调用monitorexit。

小结

通过javap反汇编我们看到synchronized使用编程了monitorentor和monitorexit两个指令.每个锁对象都会关联一个monitor(监视器,它才是真正的锁对象),它内部有两个重要的成员变量owner会保存获得锁的线程,recursions会保存线程获得锁的次数,当执行到monitorexit时,recursions会-1,当计数器减到0时这个线程就会释放锁

深入jvm源码

源码下载

http://openjdk.java.net/ --> Mercurial --> jdk8 --> hotspot --> zip

monitor监视器锁

在HotSpot虚拟机中,monitor是由ObjectMonitor实现的。其源码是用c++来实现的,位于HotSpot虚拟机源码ObjectMonitor.hpp文件中(src/share/vm/runtime/objectMonitor.hpp

ObjectMonitor主要数据结构如下

c++
ObjectMonitor() { 
    _header = NULL; 
    _count = 0;
    _waiters = 0
    _recursions = 0; // 线程的重入次数
    _object = NULL; // 存储该monitor的对象
    _owner = NULL; // 标识拥有该monitor的线程
    _WaitSet = NULL; // 处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock = 0 ;
    _Responsible = NULL; 
    _succ = NULL; 
    _cxq = NULL; // 多线程竞争锁时的单向列表 
    FreeNext = NULL;
    _EntryList = NULL; // 处于等待锁block状态的线程,会被加入到该列表 
    _SpinFreq = 0;
    _SpinClock = 0;
    OwnerIsThread = 0; 
}
  1. _owner:初始时为NULL。当有线程占有该monitor时,owner标记为该线程的唯一标识。当线程 释放monitor时,owner又恢复为NULL。owner是一个临界资源,JVM是通过CAS操作来保证其线 程安全的。
  2. _cxq:竞争队列,所有请求锁的线程首先会被放在这个队列中(单向链接)。_cxq是一个临界资 源,JVM通过CAS原子指令来修改_cxq队列。修改前_cxq的旧值填入了node的next字段,_cxq指 向新值(新线程)。因此_cxq是一个后进先出的stack(栈)。
  3. _EntryList:_cxq队列中有资格成为候选资源的线程会被移动到该队列中。
  4. _WaitSet:因为调用wait方法而被阻塞的线程会被放在该队列中。

每一个Java对象都可以与一个监视器monitor关联,我们可以把它理解成为一把锁,当一个线程想要执行一段被synchronized圈起来的同步方法或者代码块时,该线程得先获取到synchronized修饰的对象对应的monitor。

我们的Java代码里不会显示地去创造这么一个monitor对象,我们也无需创建,事实上可以这么理解:monitor并不是随着对象创建而创建的。我们是通过synchronized修饰符告诉JVM需要为我们的某个对象创建关联的monitor对象。每个线程都存在两个ObjectMonitor对象列表,分别为free和used列表。同时JVM中也维护着global locklist。当线程需要ObjectMonitor对象时,首先从线程自身的free表中申请,若存在则使用,若不存在则从global list中申请。

monitor竞争
  1. 执行monitorenter时,会调用InterpreterRuntime.cpp(位于:src/share/vm/interpreter/interpreterRuntime.cpp) 的 InterpreterRuntime::monitorenter函数。具体代码可参见HotSpot源码。
  2. 对于重量级锁,monitorenter函数中会调用 ObjectSynchronizer::slow_enter
  3. 最终调用 ObjectMonitor::enter(位于:src/share/vm/runtime/objectMonitor.cpp)

具体流程概括如下:

  1. 通过CAS尝试把monitor的owner字段设置为当前线程。
  2. 如果设置之前的owner指向当前线程,说明当前线程再次进入monitor,即重入锁,执行 recursions ++ ,记录重入的次数。
  3. 如果当前线程是第一次进入该monitor,设置recursions为1,_owner为当前线程,该线程成功获 得锁并返回。
  4. 如果获取锁失败,则等待锁的释放。
monitor等待

竞争失败等待调用的是ObjectMonitor对象的EnterI方法(位于:src/share/vm/runtime/objectMonitor.cpp)

当该线程被唤醒时,会从挂起的点继续执行,通过 ObjectMonitor::TryLock 尝试获取锁

的具体流程概括如下:

  1. 当前线程被封装成ObjectWaiter对象node,状态设置成ObjectWaiter::TS_CXQ。
  2. 在for循环中,通过CAS把node节点push到_cxq列表中,同一时刻可有多个线程把自己的node 节点push到_cxq列表中。
  3. node节点push到_cxq列表之后,通过自旋尝试获取锁,如果还是没有获取到锁,则通过park将当前线程挂起,等待被唤醒。
  4. 当该线程被唤醒时,会从挂起的点继续执行,通过 ObjectMonitor::TryLock 尝试获取锁。
monitor释放

当某个持有锁的线程执行完同步代码块时,会进行锁的释放,给其它线程机会执行同步代码,在HotSpot中,通过退出monitor的方式实现锁的释放,并通知被阻塞的线程,具体实现位于ObjectMonitor的exit方法中。(位于:src/share/vm/runtime/objectMonitor.cpp)

  1. 退出同步代码块时会让_recursions减1,当_recursions的值减为0时,说明线程释放了锁。
  2. 根据不同的策略(由QMode指定),从cxq或EntryList中获取头节点,通过ObjectMonitor::ExitEpilog 方法唤醒该节点封装的线程,唤醒操作最终由unpark完成

被唤醒的线程,会回到 void ATTR ObjectMonitor::EnterI (TRAPS) 的第600行,继续执行monitor的竞争。

monitor是重量级锁

ObjectMonitor的函数调用中会涉及到Atomic::cmpxchg_ptr,Atomic::inc_ptr等内核函数,执行同步代码块,没有竞争到锁的对象会park()被挂起,竞争到锁的线程会unpark()唤醒。这个时候就会存在操作系统用户态和内核态的转换,这种切换会消耗大量的系统资源。所以synchronized是Java语言中是一个重量级(Heavyweight)的操作。

Linux操作系统的体系架构分为:用户空间(应用程序的活动空间)和内核。

内核:本质上可以理解为一种软件,控制计算机的硬件资源,并提供上层应用程序运行的环境。

用户空间:上层应用程序活动的空间。应用程序的执行必须依托于内核提供的资源,包括CPU资源、存储资源、I/O资源等。

系统调用:为了使上层应用能够访问到这些资源,内核必须为上层应用提供访问的接口:即系统调用。

所有进程初始都运行于用户空间,此时即为用户运行状态(简称:用户态);但是当它调用系统调用执行某些操作时,例如 I/O调用,此时需要陷入内核中运行,我们就称进程处于内核运行态(或简称为内核态)。 系统调用的过程可以简单理解为:

  1. 用户态程序将一些数据值放在寄存器中, 或者使用参数创建一个堆栈, 以此表明需要操作系统提供的服务。
  2. 用户态程序执行系统调用。
  3. CPU切换到内核态,并跳到位于内存指定位置的指令。
  4. 系统调用处理器(system call handler)会读取程序放入内存的数据参数,并执行程序请求的服务。
  5. 系统调用完成后,操作系统会重置CPU为用户态并返回系统调用的结果。由此可见用户态切换至内核态需要传递许多变量,同时内核还需要保护好用户态在切换时的一些寄存器值、变量等,以备内核态切换回用户态。这种切换就带来了大量的系统资源消耗,这就是在synchronized未优化之前,效率低的原因。

JDK6 synchronized优化

CAS的全成是: Compare And Swap(比较相同再交换)。是现代CPU广泛支持的一种对内存中的共享数据进行操作的一种特殊指令。

CAS的作用:CAS可以将比较和交换转换为原子操作,这个原子操作直接由CPU保证。CAS可以保证共享变量赋值时的原子操作。CAS操作依赖3个值:内存中的值V,旧的预估值X,要修改的新值B,如果旧的预估值X等于内存中的值V,就将新的值B保存到内存中。

Unsafe类介绍

Unsafe类使Java拥有了像C语言的指针一样操作内存空间的能力,同时也带来了指针的问题。过度的使用Unsafe类会使得出错的几率变大,因此Java官方并不建议使用的,官方文档也几乎没有。Unsafe对象不能直接调用,只能通过反射获得。

CAS获取共享变量时,为了保证该变量的可见性,需要使用volatile修饰。结合CAS和volatile可以实现无锁并发,适用于竞争不激烈、多核 CPU 的场景下。

  1. 因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一。
  2. 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响

synchronized锁升级过程

高效并发是从JDK 5到JDK 6的一个重要改进,HotSpot虛拟机开发团队在这个版本上花费了大量的精力去实现各种锁优化技术,包括偏向锁( Biased Locking )、轻量级锁( Lightweight Locking )和如适应性自旋(Adaptive Spinning)、锁消除( Lock Elimination)、锁粗化( Lock Coarsening )等,这些技术都是为了在线程之间更高效地共享数据,以及解决竞争问题,从而提高程序的执行效率。

无锁--》偏向锁--》轻量级锁–》重量级锁

1.1.8 synchronized与Lock的区别

  1. synchronized是关键字,而Lock是一个接口。
  2. synchronized会自动释放锁,而Lock必须手动释放锁。
  3. synchronized是不可中断的,Lock可以中断也可以不中断。
  4. 通过Lock可以知道线程有没有拿到锁,而synchronized不能。
  5. synchronized能锁住方法和代码块,而Lock只能锁住代码块。
  6. Lock可以使用读锁提高多线程读效率。
  7. synchronized是非公平锁,ReentrantLock可以控制是否是公平锁。

1.1.9 synchronized优化

减少synchronized的范围

同步代码块中尽量短,减少同步代码块中代码的执行时间,减少锁的竞争。

降低synchronized锁的粒度

将一个锁拆分为多个锁提高并发度

读写分离

读取时不加锁,写入和删除时加锁 ConcurrentHashMap,CopyOnWriteArrayList和ConyOnWriteSet

1.2 volatile关键字

变量的线程可见性。在CPU计算过程中,会将计算过程需要的数据加载到CPU计算缓存中,当CPU计算中断时,有可能刷新缓存,重新读取内存中的数据。在线程运行的过程中,如果某变量被其他线程修改,可能造成数据不一致的情况,从而导致结果错误。而volatile修饰的变量是线程可见的,当JVM解释volatile修饰的变量时,会通知CPU,在计算过程中,每次使用变量参与计算时,都会检查内存中的数据是否发生变化,而不是一直使用CPU缓存中的数据,可以保证计算结果的正确。

volatile只是通知底层计算时,CPU检查内存数据,而不是让一个变量在多个线程中同步。

  1. volatile 修饰符适用于以下场景:某个属性被多个线程共享,其中有一个线程修改了此属性,其他线程可以立即得到修改后的值,比如boolean flag ;或者作为触发器,实现轻量级同步。
  2. volatile属性的读写操作都是无锁的,它不能替代synchronized ,因为它没有提供原子性和互斥性。因为无锁,不需要花费时间在获取锁和释放锁_上,所以说它是低成本的。
  3. volatile只能作用于属性,我们用volatile修饰属性,这样compilers就不会对这个属性做指令重排序。
  4. volatile 提供了可见性,任何一个线程对其的修改将立马对其他线程可见。volatile 属性不会被线程缓存,始终从主存中读取。
  5. volatile提供了happens-before保证,对volatile变量v的写入happens- before所有其他线程后续对v的读操作。
  6. volatile可以使得long和double的赋值是原子的。
  7. volatile可以在单例双重检查中实现可见性和禁止指令重排序,从而保证安全性。

工作原理

  1. 子线程t从主内存读取到数据放入其对应的工作内存

  2. 将flag的值更改为true,但是这个时候flag的值还没有写会主内存

  3. 此时main方法main方法读取到了flag的值为false

  4. 当子线程t将flag的值写回去后,失效其他线程对此变量副本

  5. 再次对flag进行操作的时候线程会从主内存读取最新的值,放入到工作内存中

volatile保证不同线程对共享变量操作的可见性,也就是说一个线程修改了volatile修饰的变量,当修改写回主内存时,另外一个线程立即看到最新的值

特性

volatile的原子性问题:volatile不能保证原子性操作。 禁止指令重排序:volatile可以防止指令重排序操作。

volatile不保证原子性

所谓的原子性是指在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行。volatile不保证原子性。

在多线程环境下,volatile关键字可以保证共享数据的可见性,但是并不能保证对数据操作的原子性(在多线程环境下volatile修饰的变量也是线程不安全的)。 在多线程环境下,要保证数据的安全性,我们还需要使用锁机制。

可以使用原子类进行操作

禁止指令重排序

什么是重排序:为了提高性能,编译器和处理器常常会对既定的代码执行顺序进行指令重排序。 原因:一个好的内存模型实际上会放松对处理器和编译器规则的束缚,也就是说软件技术和硬件技术都为同一个目标而进行奋斗:在不改变程序执行结果的前提下,尽可能提高执行效率。JMM对底层尽量减少约束,使其能够发挥自身优势。因此,在执行程序时,为了提高性能,编译器和处理器常常会对指令进行重排序。一般重排序可以分为如下三种: 1.编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序; 2.指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序; 3.内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行的。

重排序可以提高处理的速度。

重排问题

重排序虽然可以提高执行的效率,但是在并发执行下,JVM虚拟机底层并不能保证重排序下带来的安全性等问题

volatile禁止重排序

volatile修饰变量后可以实现禁止指令重排序!

volatile内存语义

volatile写读建立的happens-before关系

从JDK 5开始,提出了happens-before的概念,通过这个概念来阐述操作之间的内存可见性。如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。

为了解决多线程的可见性问题,就搞出了happens-before原则,让线程之间遵守这些原则。编译器还会优化我们的语句

happens-before 应该翻译成:前一个操作的结果可以被后续的操作获取。讲白点就是前面一个操作变量a赋值为1,那后面一个操作肯定能知道a已经变成了1。

happens-before规则

具体的一共有六项规则:

  1. 程序顺序规则(单线程规则) 解释:一个线程中的每个操作,happens-before于该线程中的任意后续操作 同一个线程中前面的所有写操作对后面的操作可见

  2. 锁规则(Synchronized,Lock等) 解释:对一个锁的解锁,happens-before于随后对这个锁的加锁。 如果线程1解锁了monitor a,接着线程2锁定了a,那么,线程1解锁a之前的写操作都对线程2可见(线程1和线程2可以是同一个线程)

  3. volatile变量规则: 解释:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。 如果线程1写入了volatile变量v(临界资源),接着线程2读取了v,那么,线程1写入v及之前的写操作都对线程2可见(线程1和线程2可以是同一个线程)

  4. 传递性: 解释:如果A happens-before B,且B happens-before C,那么A happens-before C。 A h-b B , B h-b C 那么可以得到 A h-b C

  5. start()规则: 解释:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happensbefore于线程B中的任意操作。 假定线程A在执行过程中,通过执行ThreadB.start()来启动线程B,那么线程A对共享变量的修改在接下来线程B开始执行前对线程B可见。注意:线程B启动之后,线程A在对变量修改线程B未必可见

  6. join()规则

    解释:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。 线程t1写入的所有变量,在任意其它线程t2调用t1.join(),或者t1.isAlive() 成功返回后,都对t2可见

volatile写读建立的happens-before规则

happens-before有一个原则是:如果A是对volatile变量的写操作,B是对同一个变量的读操作,那么hb(A,B)

volatile重排序规则

写volatile变量时,无论前一个操作是什么,都不能重排序 读volatile变量时,无论后一个操作是什么,都不能重排序 当先写volatile变量,后读volatile变量时,不能重排序

long和double的原子性

在java中,long和double都是8个字节共64位(一个字节=8bit),那么如果是一个32位的系统,读写long或double的变量时会涉及到原子性问题,因为32位的系统要读完一个64位的变量,需要分两步执行,每次读取32位,这样就对double和long变量的赋值就会出现问题: 如果有两个线程同时写一个变量内存,一个进程写低32位,而另一个写高32位,这样将导致获取的64位数据是失效的数据

如果是在64位的系统中,那么对64位的long和double的读写都是原子操作的。即可以以一次性读写long或double的整个64bit。如果在32位的JVM上,long和double就不是原子性操作了。

需要使用volatile关键字来防止此类现象

在双重检查加锁的单例中的应用

java
public class Singleton6 {
    // 静态属性,volatile保证可见性和禁止指令重排序 
    private volatile static Singleton6 instance = null;
    // 私有化构造器
    private Singleton6(){} 
    public static Singleton6 getInstance(){ 
        // 第一重检查锁定
        if(instance==null){
            // 同步锁定代码块 
            synchronized(Singleton6.class){ 
                // 第二重检查锁定 
                if(instance==null){ 
                    // 注意:非原子操作 
                    instance=new Singleton6();
                } 
            }
        }
        return instance; 
    }
}

双重检查的优点:线程安全,延迟加载,效率较高!

使用场景

纯赋值操作

volatile不适合做a++等操作。 适合做纯赋值操作:如 boolean flag = false/true;

如果一个共享变量自始至终只被各个线程赋值,而没有其他的操作,那么就可以用volatile来代替synchronized或者代替原子变量,因为赋值自身是有原子性的,而volatile又保证了可见性,所以就足以保证线程安全。

触发器

按照volatile的可见性和禁止重排序以及happens-before规则,volatile可以作为刷新之前变量的触发器。我们可以将某个变量设置为volatile修饰,其他线程一旦发现该变量修改的值后,触发获取到的该变量之前的操作都将是最新的且可见

volatile与synchronized

区别

volatile只能修饰实例变量和类变量,而synchronized可以修饰方法,以及代码块。 volatile保证数据的可见性,但是不保证原子性(多线程进行写操作,不保证线程安全);而synchronized是一种排他(互斥)的机制。 volatile用于禁止指令重排序:可以解决单例双重检查对象初始化代码执行乱序问题。 volatile可以看做是轻量版的synchronized,volatile不保证原子性,但是如果是对一个共享变量进行多个线程的赋值,而没有其他的操作,那么就可以用volatile来代替synchronized,因为赋值本身是有原子性的,而volatile又保证了可见性,所以就可以保证线程安全了。

1.3 AtomicXxx类型组

原子类型。

在concurrent.atomic包中定义了若干原子类型,这些类型中的每个方法都是保证了原子操作的。多线程并发访问原子类型对象中的方法,不会出现数据错误。在多线程开发中,如果某数据需要多个线程同时操作,且要求计算原子性,可以考虑使用原子类型对象。

注意:原子类型中的方法是保证了原子操作,但多个方法之间是没有原子性的。如: AtomicInteger i = new AtomicInteger(0); if(i.get() != 5) i.incrementAndGet();

在上述代码中,get方法和incrementAndGet方法都是原子操作,但复合使用时,无法保证原子性,仍旧可能出现数据错误。

1.4 CountDownLatch门闩

门闩是concurrent包中定义的一个类型,是用于多线程通讯的一个辅助类型。 门闩相当于在一个门上加多个锁,当线程调用await方法时,会检查门闩数量,如果门闩数量大于0,线程会阻塞等待。当线程调用countDown时,会递减门闩的数量,当门闩数量为0时,await阻塞线程可执行。

1.5 锁的重入

在Java中,同步锁是可以重入的。只有同一线程调用同步方法或执行同步代码块,对同一个对象加锁时才可重入。

当线程持有锁时,会在monitor的计数器中执行递增计算,若当前线程调用其他同步代码,且同步代码的锁对象相同时,monitor中的计数器继续递增。每个同步代码执行结束,monitor中的计数器都会递减,直至所有同步代码执行结束,monitor中的计数器为0时,释放锁标记,_Owner标记赋值为null。

1.6 ReentrantLock

重入锁,建议应用的同步方式。相对效率比synchronized高。量级较轻。

synchronized在JDK1.5版本开始,尝试优化。到JDK1.7版本后,优化效率已经非常好了。在绝对效率上,不比reentrantLock差多少。

使用重入锁,必须必须必须手工释放锁标记。一般都是在finally代码块中定义释放锁标记的unlock方法。

1.6.1 公平锁

image-20210124115121406

1.7 ThreadLocal

remove问题

image-20210124115207039

二、同步容器

解决并发情况下的容器线程安全问题的。给多线程环境准备一个线程安全的容器对象。

线程安全的容器对象: Vector, Hashtable。线程安全容器对象,都是使用synchronized方法实现的。

concurrent包中的同步容器,大多数是使用系统底层技术实现的线程安全。类似native。Java8中使用CAS。

2.1 Map/Set

2.1.1 ConcurrentHashMap/ConcurrentHashSet

底层哈希实现的同步Map(Set)。效率高,线程安全。使用系统底层技术实现线程安全。

量级较synchronized低。key和value不能为null。

2.1.2 ConcurrentSkipListMap/ConcurrentSkipListSet

底层跳表(SkipList)实现的同步Map(Set)。有序,效率比ConcurrentHashMap稍低。

image-20210124115415980

2.2 List

2.2.1 CopyOnWriteArrayList

写时复制集合。写入效率低,读取效率高。每次写入数据,都会创建一个新的底层数组。

2.3 Queue

2.3.1 ConcurrentLinkedQueue

基础链表同步队列。

2.3.2 LinkedBlockingQueue

阻塞队列,队列容量不足自动阻塞,队列容量为0自动阻塞。

2.3.3 ArrayBlockingQueue

底层数组实现的有界队列。自动阻塞。根据调用API(add/put/offer)不同,有不同特性。

当容量不足的时候,有阻塞能力。

add方法在容量不足的时候,抛出异常。

put方法在容量不足的时候,阻塞等待。

offer方法,

单参数offer方法,不阻塞。容量不足的时候,返回false。当前新增数据操作放弃。

三参数offer方法(offer(value,times,timeunit)),容量不足的时候,阻塞times时长(单位为timeunit),如果在阻塞时长内,有容量空闲,新增数据返回true。如果阻塞时长范围内,无容量空闲,放弃新增数据,返回false。

2.3.4 DelayQueue

延时队列。根据比较机制,实现自定义处理顺序的队列。常用于定时任务。 如:定时关机。

2.3.5 LinkedTransferQueue

转移队列,使用transfer方法,实现数据的即时处理。没有消费者,就阻塞。

2.3.6 SynchronusQueue

同步队列,是一个容量为0的队列。是一个特殊的TransferQueue。

必须现有消费线程等待,才能使用的队列。

add方法,无阻塞。若没有消费线程阻塞等待数据,则抛出异常。

put方法,有阻塞。若没有消费线程阻塞等待数据,则阻塞。

三、ThreadPool&Executor

3.1 Executor

线程池顶级接口。定义方法,void execute(Runnable)。方法是用于处理任务的一个服务方法。调用者提供Runnable接口的实现,线程池通过线程执行这个Runnable。服务方法无返回值的。是Runnable接口中的run方法无返回值。

常用方法 - void execute(Runnable)

作用是: 启动线程任务的。

3.2 ExecutorService

Executor接口的子接口。提供了一个新的服务方法,submit。有返回值(Future类型)。submit方法提供了overload方法。其中有参数类型为Runnable的,不需要提供返回值的;有参数类型为Callable,可以提供线程执行后的返回值。

Future,是submit方法的返回值。代表未来,也就是线程执行结束后的一种结果。如返回值。

常见方法 - void execute(Runnable), Future submit(Callable), Future submit(Runnable)

线程池状态: Running, ShuttingDown, Termitnaed

Running - 线程池正在执行中。活动状态。

ShuttingDown - 线程池正在关闭过程中。优雅关闭。一旦进入这个状态,线程池不再接收新的任务,处理所有已接收的任务,处理完毕后,关闭线程池。

Terminated - 线程池已经关闭。

3.3 Future

未来结果,代表线程任务执行结束后的结果。获取线程执行结果的方式是通过get方法获取的。get无参,阻塞等待线程执行结束,并得到结果。get有参,阻塞固定时长,等待线程执行结束后的结果,如果在阻塞时长范围内,线程未执行结束,抛出异常。

常用方法: T get() T get(long, TimeUnit)

3.4 Callable

可执行接口。 类似Runnable接口。也是可以启动一个线程的接口。其中定义的方法是call。call方法的作用和Runnable中的run方法完全一致。call方法有返回值。

接口方法 : Object call();相当于Runnable接口中的run方法。区别为此方法有返回值。不能抛出已检查异常。

和Runnable接口的选择 - 需要返回值或需要抛出异常时,使用Callable,其他情况可任意选择。

3.5 Executors

工具类型。为Executor线程池提供工具方法。可以快速的提供若干种线程池。如:固定容量的,无限容量的,容量为1等各种线程池。

线程池是一个进程级的重量级资源。默认的生命周期和JVM一致。当开启线程池后,直到JVM关闭为止,是线程池的默认生命周期。如果手工调用shutdown方法,那么线程池执行所有的任务后,自动关闭。

开始 - 创建线程池。

结束 - JVM关闭或调用shutdown并处理完所有的任务。

类似Arrays,Collections等工具类型的功用。

3.6 FixedThreadPool

容量固定的线程池。活动状态和线程池容量是有上限的线程池。所有的线程池中,都有一个任务队列。使用的是BlockingQueue<Runnable>作为任务的载体。当任务数量大于线程池容量的时候,没有运行的任务保存在任务队列中,当线程有空闲的,自动从队列中取出任务执行。

使用场景: 大多数情况下,使用的线程池,首选推荐FixedThreadPool。OS系统和硬件是有线程支持上限。不能随意的无限制提供线程池。

线程池默认的容量上限是Integer.MAX_VALUE。

常见的线程池容量: PC - 200。 服务器 - 1000~10000

queued tasks - 任务队列

completed tasks - 结束任务队列

3.7 CachedThreadPool

缓存的线程池。容量不限(Integer.MAX_VALUE)。自动扩容。容量管理策略:如果线程池中的线程数量不满足任务执行,创建新的线程。每次有新任务无法即时处理的时候,都会创建新的线程。当线程池中的线程空闲时长达到一定的临界值(默认60秒),自动释放线程。

默认线程空闲60秒,自动销毁。

应用场景: 内部应用或测试应用。 内部应用,有条件的内部数据瞬间处理时应用,如:

电信平台夜间执行数据整理(有把握在短时间内处理完所有工作,且对硬件和软件有足够的信心)。 测试应用,在测试的时候,尝试得到硬件或软件的最高负载量,用于提供FixedThreadPool容量的指导。

3.8 ScheduledThreadPool

计划任务线程池。可以根据计划自动执行任务的线程池。

scheduleAtFixedRate(Runnable, start_limit, limit, timeunit)

runnable - 要执行的任务。

start_limit - 第一次任务执行的间隔。

limit - 多次任务执行的间隔。

timeunit - 多次任务执行间隔的时间单位。

使用场景: 计划任务时选用(DelaydQueue),如:电信行业中的数据整理,没分钟整理,没消失整理,每天整理等。

3.9 SingleThreadExceutor

单一容量的线程池。 使用场景: 保证任务顺序时使用。如: 游戏大厅中的公共频道聊天。秒杀。

3.10 ForkJoinPool

分支合并线程池(mapduce类似的设计思想)。适合用于处理复杂任务。

初始化线程容量与CPU核心数相关。

线程池中运行的内容必须是ForkJoinTask的子类型(RecursiveTask,RecursiveAction)。

ForkJoinPool - 分支合并线程池。 可以递归完成复杂任务。要求可分支合并的任务必须是ForkJoinTask类型的子类型。其中提供了分支和合并的能力。ForkJoinTask类型提供了两个抽象子类型,RecursiveTask有返回结果的分支合并任务,RecursiveAction无返回结果的分支合并任务。(Callable/Runnable)compute方法:就是任务的执行逻辑。

ForkJoinPool没有所谓的容量。默认都是1个线程。根据任务自动的分支新的子线程。当子线程任务结束后,自动合并。所谓自动是根据fork和join两个方法实现的。

应用: 主要是做科学计算或天文计算的。数据分析的。

3.11 WorkStealingPool

JDK1.8新增的线程池。工作窃取线程池。当线程池中有空闲连接时,自动到等待队列中窃取未完成任务,自动执行。

初始化线程容量与CPU核心数相关。此线程池中维护的是精灵线程。

ExecutorService.newWorkStealingPool();

3.12 ThreadPoolExecutor

线程池底层实现。除ForkJoinPool外,其他常用线程池底层都是使用ThreadPoolExecutor实现的。

public ThreadPoolExecutor

(int corePoolSize, // 核心容量,创建线程池的时候,默认有多少线程。也是线程池保持的最少线程数

int maximumPoolSize, // 最大容量,线程池最多有多少线程

long keepAliveTime, // 生命周期,0为永久。当线程空闲多久后,自动回收。

TimeUnit unit, // 生命周期单位,为生命周期提供单位,如:秒,毫秒

BlockingQueue<Runnable> workQueue // 任务队列,阻塞队列。注意,泛型必须是Runnable );

使用场景: 默认提供的线程池不满足条件时使用。如:初始线程数据4,最大线程数200,线程空闲周期30秒。

四、JVM优化

JVM并没有直接与硬件打交道,而是与操作系统交互用以执行java程序

运行流程

image-20220709172718849

  • 类加载器

    ​ 类加载器的作用是加载类文件到内存。比如我们写一个HelloWorld.java的程序,首先使用javac命令进行编译,生成HelloWorld.java的字节码文件,

    怎样才能执行.class文件呢。就需要用药类加载器将字节码文件加载到内存中,然后通过jvm后续的模块进行加载执行程序。ClassLoader只管加载,

    至于是否能够执行,则不属于它的负责范围,由执行引擎负责。

  • 执行引擎

    ​ 执行引擎也叫解释器,负责解释命令,提交操作系统执行

  • 本地接口

    ​ 本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序,Java诞生的时候是C/C++横

    行的时候,要想立足,必须有一个聪明的、睿智的调用C/C++程序,于是就在内存中专门开辟了一块区域处理

    标记为native的代码,它的具体做法是Native Method Stack中登记native方法,在Execution Engine执行时

    加载native libraies。目前该方法使用的是越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动打印

    机,或者Java系统管理生产设备,在企业级应用中已经比较少见,因为现在的异构领域间的通信很发达,比如

    可以使用Socket通信,也可以使用Web Service等等,不多做介绍。

  • 运行时数据区

    ​ 运行数据区是整个JVM的重点。我们所有写的程序都被加载到这里,之后才开始运行,Java生态系统如此的繁

    荣,得益于该区域的优良自治。整个JVM框架由加载器加载文件,然后执行器在内存中处理数据,需要与异构系统交互是可以通过本地接口进行!

运行时数据区

程序计数器:

​ 程序计数器是一小块的内存区域,可以看做当前线程执行字节码的行号指示器,在虚拟机的概念模型里,字节码解释工作就是通过改变这个计数器的值来选取下一个要

​ 执行的字节码指令。比如分支控制,循环控制,跳转,异常等操作,线程恢复等功能都是通过这个计数器来完成。由于jvm的多线程是通过线程的轮流切换并分配处理器

​ 执行时间来实现的。因此,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能回到正确的

​ 执行位置,每条线程都需要自己独有的程序计数器,多条线程计数器之间互不影响,独立存储。我们称这类内存区域为线程私有的内存区域。

​ 如果线程正在执行一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行native方法,则这个计数器则为空(undefined)

​ 此内存区域是Java中虚拟机中唯一一个没有规定任何OutOfMemoryError的内存区域。

Java虚拟机栈:

每个线程运行时所需要的内存空间,称为虚拟机栈

每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存,栈帧由局部变量表、操作数栈、动态链接、方法返回地址组成。简单的理解为指向运行时常量池的引用

每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

​ 与程序计数器一致,Java虚拟机栈也是线程私有的,生命周期与线程相同。虚拟机栈描述的是方法的执行内存模型,每个方法在执行的时候都会创建一个栈帧,用于存储局部

​ 变量表,操作数栈,方法出口等信息。每一个方法从执行到结束的过程,就对应一个栈帧从入栈到出栈的过程。

​ 局部变量表存放了编译器可知的四类八种基本数据类型,对象引用(refrence),它不等同于对象本身,可能是指向对象起始地址的引用指针。

​ 局部变量表的内存分配在编译期已经完成分配了,其中64位长度的long和double会占用两个局部变量空间,其余的数据类型只占一个。当进入一个方法时,这个方法需要在栈中分配

​ 多大的内存空间是完全能够确定的,方法运行期间不改变局部变量表的大小。

​ 如果线程在栈中申请的深度大于虚拟机所允许的深度,将出现StackOverFlowError异常; 如果虚拟机栈可以动态扩展(当前大部分虚拟机支持动态扩展,当然也允许固定长度的虚拟机栈),如果

​ 扩展无法申请到足够的内存,

​ 就会抛出OutOfMemoryError异常。

本地方法栈:

​ 本地方法栈与虚拟机栈的作用非常类似,只不过虚拟机栈执行的是Java方法,而本地方法栈执行的是本地native方法,在虚拟机规范中并没有对本地方法栈中方法使用的语言,使用方式与数

​ 据结构并没有强行规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机,如Sun的Hotspot虚拟机直接将虚拟机栈和本地方法栈合二为一。当然与虚拟机栈一样,本地方法栈也会抛出

​ StackOverFlowError异常和OutOfMemoryError异常。

Java堆

​ 对于大多数应用来说,Java堆(Java Heap)是JVM所管理的内存中最大的一块区域,且Java堆是被所有线程所共享的一片区域,在虚拟机启动时创建。该区域的唯一目的就是存放实例对象,

几乎所有的对象实例都在这里分配空间。这一点在JVM规范上描述的是:所有的对象实例以及数组都要在堆上分配空间。

​ Java堆是垃圾收集器管理的管理的主要区域,因此很多时候被称为GC堆。从内存分配的角度讲,由于现在的垃圾回收机制都是分代垃圾回收,所以堆中可以再划分为老年代和新生代,

再细的划分为Eden区,Survivor区,其中Survivor区又可细分为From Survivor区和To Survivor区。根据JVM的规范规定,Java堆可以处于物理上不连续的内存空间,只要逻辑上是连续的即可。

就像我们的磁盘一样,既可以是固定大小的,也可以是可扩展的。不过当前主流的都采用可扩展的策略(采用-Xmx 和 -Xms控制)。如果在堆中没有完成内存分配,且堆也没有可扩展的内存空间,

则会抛出OutOfMemoryError异常。

方法区:

​ 方法区与java堆一样,有各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息,常量,静态变量,及时编译器编译后的代码等数据。Java虚拟机相对而言对方法区的限制非常宽

松,除了和堆一样不需要连续的空间和可以选择固定大小或者可扩展之外,还可以选择不实现垃圾回收。相对而言,垃圾回收在这个区域算比较少见了,但并非数据进入方法区以后就可以实现永

久存活了,这个区域的回收目标主要是常量池的回收和对类型的卸载,一般来说,这个区域的回收成绩是比较难以让人满意的。尤其是类型的卸载,条件相当苛刻。根据Java虚拟机规范规定,当

方法区无法满足内存分配时,将抛出OutOfMemoryError异常。

运行时常量池:

​ 运行时常量池是方法区的一部分,Class文件中除了有类的版本,字段,方法和接口的信息外,还有一项信息是常量池。用于存放编译器各种字面量和符号的引用,这部分内容将在类加载后

进入到常量池中存储。一般来说,除了保存Class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。运行时常量池相对于Class文件的常量池一个最大的特性就是

动态性,Java语言并不要求常量一定在编译期间产生,也就是说并非预置入Class文件中常量池的内容才能进入常量池,在运行期间也可能将新产生的常量放进常量池,这种特性被利用最多的就

是String的intern()方法。既然运行时常量池属于方法区的一部分,自然具备方法区的约束,所以当内存申请不到的时候也会抛出OutOfMemoryError异常。

直接内存:

​ 直接内存并不属于Jvm运行时数据区的一部分,但是这部分内存区域被频繁的调用,也可能发生OutOfMemoryError异常,所以一起讨论。显然本机的直接内存不会受到Java堆分配内存的影

响,但是既然是内存,肯定要受到本机总内存的限制。服务器管理员在配置虚拟机参数时,会根据实际内存设置-Xmx等参数信息,但经常忽略直接内存。使得各个区域的内存总和大于物理内存

限制,从而导致动态扩展时出现OutOfMemoryError异常。

4.1 JVM简单结构图

image-20210124120352353

4.1.1 类加载子系统与方法区

类加载子系统负责从文件系统或者网络中加载Class信息,加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中可能还会存放运行时常量池信息,包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)。

4.1.2 Java堆

java堆在虚拟机启动的时候建立,它是java程序最主要的内存工作区域。几乎所有的java对象实例都存放在java堆中。堆空间是所有线程共享的,这是一块与java应用密切相关的内存空间。

4.1.3 直接内存

java的NIO库允许java程序使用直接内存。直接内存是在java堆外的、直接向系统申请的内存空间。通常访问直接内存的速度会优于java堆。因此出于性能的考虑,读写频繁的场合可能会考虑使用直接内存。由于直接内存在java堆外,因此它的大小不会直接受限于Xmx指定的最大堆大小,但是系统内存是有限的,java堆和直接内存的总和依然受限于操作系统能给出的最大内存。

4.1.4 垃圾回收系统

垃圾回收系统是java虚拟机的重要组成部分,垃圾回收器可以对方法区、java堆和直接内存进行回收。其中,java堆是垃圾收集器的工作重点。和C/C++不同,java中所有的对象空间释放都是隐式的,也就是说,java中没有类似free()或者delete()这样的函数释放指定的内存区域。对于不再使用的垃圾对象,垃圾回收系统会在后台默默工作,默默查找、标识并释放垃圾对象,完成包括java堆、方法区和直接内存中的全自动化管理。

4.1.5 Java栈

每一个java虚拟机线程都有一个私有的java栈,一个线程的java栈在线程创建的时候被创建,java栈中保存着帧信息,java栈中保存着局部变量、方法参数,同时和java方法的调用、返回密切相关。

4.1.6 本地方法栈

本地方法栈和java栈非常类似,最大的不同在于java栈用于方法的调用,而本地方法栈则用于本地方法的调用,作为对java虚拟机的重要扩展,java虚拟机允许java直接调用本地方法(通常使用C编写)

4.1.7 PC寄存器

PC(Program Counter)寄存器也是每一个线程私有的空间,java虚拟机会为每一个java线程创建PC寄存器。在任意时刻,一个java线程总是在执行一个方法,这个正在被执行的方法称为当前方法。如果当前方法不是本地方法,PC寄存器就会指向当前正在被执行的指令。如果当前方法是本地方法,那么PC寄存器的值就是undefined

4.1.8 执行引擎

执行引擎是java虚拟机的最核心组件之一,它负责执行虚拟机的字节码,现代虚拟机为了提高执行效率,会使用即时编译(just in time)技术将方法编译成机器码后再执行。

Java HotSpot Client VM(-client),为在客户端环境中减少启动时间而优化的执行引擎;本地应用开发使用。(如:eclipse)

Java HotSpot Server VM(-server),为在服务器环境中最大化程序执行速度而设计的执行引擎。应用在服务端程序。(如:tomcat)

Java HotSpot Client模式和Server模式的区别

当虚拟机运行在-client模式的时候,使用的是一个代号为C1的轻量级编译器, 而-server模式启动的虚拟机采用相对重量级,代号为C2的编译器. C2比C1编译器编译的相对彻底,服务起来之后,性能更高

JDK安装目录/jre/lib/(x86、i386、amd32、amd64)/jvm.cfg

文件中的内容,-server和-client哪一个配置在上,执行引擎就是哪一个。如果是JDK1.5版本且是64位系统应用时,-client无效。

--64位系统内容

-server KNOWN

-client IGNORE

--32位系统内容

-server KNOWN

-client KNOWN

注意:在部分JDK1.6版本和后续的JDK版本(64位系统)中,-client参数已经不起作用了,Server模式成为唯一

4.2 堆结构及对象分代

4.2.1 什么是分代,分代的必要性是什么

Java虚拟机根据对象存活的周期不同,把堆内存划分为几块,一般分为新生代、老年代和永久代(对HotSpot虚拟机而言),这就是JVM的内存分代策略。

堆内存是虚拟机管理的内存中最大的一块,也是垃圾回收最频繁的一块区域,我们程序所有的对象实例都存放在堆内存中。给堆内存分代是为了提高对象内存分配和垃圾回收的效率。试想一下,如果堆内存没有区域划分,所有的新创建的对象和生命周期很长的对象放在一起,随着程序的执行,堆内存需要频繁进行垃圾收集,而每次回收都要遍历所有的对象,遍历这些对象所花费的时间代价是巨大的,会严重影响我们的GC效率。

有了内存分代,情况就不同了,新创建的对象会在新生代中分配内存,经过多次回收仍然存活下来的对象存放在老年代中,静态属性、类信息等存放在永久代中,新生代中的对象存活时间短,只需要在新生代区域中频繁进行GC,老年代中对象生命周期长,内存回收的频率相对较低,不需要频繁进行回收,永久代中回收效果太差,一般不进行垃圾回收,还可以根据不同年代的特点采用合适的垃圾收集算法。分代收集大大提升了收集效率,这些都是内存分代带来的好处。

4.2.2 分代的划分

Java虚拟机将堆内存划分为新生代、老年代和永久代,永久代是HotSpot虚拟机特有的概念(JDK1.8之后为metaspace替代永久代),它采用永久代的方式来实现方法区,其他的虚拟机实现没有这一概念,而且HotSpot也有取消永久代的趋势,在JDK 1.7中HotSpot已经开始了“去永久化”,把原本放在永久代的字符串常量池移出。永久代主要存放常量、类信息、静态变量等数据,与垃圾回收关系不大,新生代和老年代是垃圾回收的主要区域。

内存简图如下:

5-1611461527745

4.2.2.1 新生代(Young Generation)

新生成的对象优先存放在新生代中,新生代对象朝生夕死,存活率很低,在新生代中,常规应用进行一次垃圾收集一般可以回收70% ~ 95% 的空间,回收效率很高。

HotSpot将新生代划分为三块,一块较大的Eden(伊甸)空间和两块较小的Survivor(幸存者)空间,默认比例为8:1:1。划分的目的是因为HotSpot采用复制算法来回收新生代,设置这个比例是为了充分利用内存空间,减少浪费。新生成的对象在Eden区分配(大对象除外,大对象直接进入老年代),当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC。

GC开始时,对象只会存在于Eden区和From Survivor区,To Survivor区是空的(作为保留区域)。GC进行时,Eden区中所有存活的对象都会被复制到To Survivor区,而在From Survivor区中,仍存活的对象会根据它们的年龄值决定去向,年龄值达到年龄阀值(默认为15,新生代中的对象每熬过一轮垃圾回收,年龄值就加1,GC分代年龄存储在对象的header中)的对象会被移到老年代中,没有达到阀值的对象会被复制到To Survivor区。接着清空Eden区和From Survivor区,新生代中存活的对象都在To Survivor区。接着, From Survivor区和To Survivor区会交换它们的角色,也就是新的To Survivor区就是上次GC清空的From Survivor区,新的From Survivor区就是上次GC的To Survivor区,总之,不管怎样都会保证To Survivor区在一轮GC后是空的。GC时当To Survivor区没有足够的空间存放上一次新生代收集下来的存活对象时,需要依赖老年代进行分配担保,将这些对象存放在老年代中。

4.2.2.2 老年代(Old Generationn)

在新生代中经历了多次(具体看虚拟机配置的阀值)GC后仍然存活下来的对象会进入老年代中。老年代中的对象生命周期较长,存活率比较高,在老年代中进行GC的频率相对而言较低,而且回收的速度也比较慢。

4.2.2.3 永久代(Permanent Generationn)

永久代存储类信息、常量、静态变量、即时编译器编译后的代码等数据,对这一区域而言,Java虚拟机规范指出可以不进行垃圾收集,一般而言不会进行垃圾回收。

4.3 垃圾回收算法及分代垃圾收集器

判断对象已死的算法

引用计数器算法

引用计数器算法简单概括为:给对象添加一个引用计数器,每当有一个地方引用该对象时,计数器+1,当引用失效时,计数器-1,任何时刻,当计数器为0

的时候,该对象不再被引用。客观的说,引用计数器的实现简单,判定效率也高,大部分场景下是一个不错的选择。但是,当前主流的Jvm均没有采用标记

清除算法,原因在于,它很难解决对象之间互相循环调用的情况

可达性分析算法

在主流的商用程序语言(如C#, Java)的主流实现中,都是通过可达性分析来判断对象是否存活,这个算法的思想就是通过一系列的成为"GC Roots"的对象作为起始点,

​ 从这些节点开始向下搜索,搜索所走过的路径成为引用链,当一个对象到"GC Roots"没有任何引用链相连,则证明此对象是不可用的。

常说的GC(Garbage Collector) roots,特指的是垃圾收集器(Garbage Collector)的对象,GC会收集那些不是GC roots且没有被GC roots引用的对象。

  • 在Java中,可以作为GC Roots的对象包括下面几种:

    • 虚拟机栈中引用的对象;
    • 方法区中类静态属性引用的对象;
    • 方法区中的常量引用的对象;
    • 本地方法栈中JNI(即一般说的Native方法)的引用的对象;

即使在可达性分析中,没有引用链到达GC Roots,也并非是“非死不可”的。这个时候对象处于缓刑阶段,要正式宣告死亡,至少要经历两次标记的过程。如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并进行一次筛选,筛选的条件是此对象是否要执行finalize()方法,当对象么有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为不必要执行finalize()方法。

​ 如果该对象被判定要执行finalize()方法,那么这个对象会被放在一个叫F-Queue的队列中,并在稍后有一个虚拟机自行创建的,优先级较低的线程去执行它,这里的执行是指会触发finalize()方法,但并不会等待它执行结束。这样做的原因是如果一个对象在执行finalize()时非常缓慢,或者执行了死循环,这样就会导致F-Queue中的其他对象处于等待中,严重的会导致整个垃圾回收系统崩溃。finalize()是对象逃脱死亡的最后一次机会,稍后GC将对F-Queue中的对象进行二次标记,如果对象要在finalize()中拯救自己的话,只能重新与引用链上的任意一个对象建立关联即可,比如把对象自己(this关键字)赋值给其他成员变量或者对象,那在第二次标记时就被移出即将回收的集合,如果没有关联上,基本可以确定要被回收了。

引用

无论是通过引用计数器判断的引用数量,还是通过可达性分析判断出的引用链是否可达,判定对象是否存活都跟引用有关。在JDK1.2以前,引用被定义为当一个reference类型的数据代表的是另外一块内存的起始地址,该类型的数据被称之为引用,这种定义很纯粹,但是也很狭隘,一个对象在这种定义下只有被引用和没有被引用两种状态。对于描述一些“食之无味,弃之可惜”的对象就显得无能为力。我们希望能描述这类对象,当内存足够的时候,将它存放在内存中,当内存空间进行垃圾回收后显得还是内存紧张时,可以回收这部分对象,很多系统的缓存功能都符合这样的应用场景。因此在JDK1.2以后对引用进行重新的扩充,分为强引用,软引用,弱引用,虚引用4中,这四种引用的强度依次递减。

强引用:

​ 强引用是在代码中普遍存在的,类似于Object obj = new Object(),只要强引用一直存在,垃圾收集器就永远不会回收被引用的对象。

软引用:

​ 软引用用来描述一些还有用但并非必须的对象,对于软引用关联着的对象,当内存溢出异常发生之前,通过垃圾回收进行二次回收。如果二次回收完成之后,系统内存依然不够,才会抛出

​ 内存溢出异常,在jdk1,2以后用SoftReference类来实现软引用。

弱引用:

​ 弱引用也是用来描述非必须对象的,但是它的强度相比于软引用来说更弱一些,它仅仅能生存到下一次垃圾回收之前。当垃圾收集时,无论内存是否足够,弱引用的对象都要被回收,

​ 在jdk1.2以后用WeakReference类来实现弱引用

虚引用:

​ 虚引用是最弱的一种引用关系,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过一个虚引用来获取一个实例对象。为一个对象设置弱引用的唯一目的就是

4.3.1 垃圾收集器的分类

4.3.1.1 次收集器

Scavenge GC,指发生在新生代的GC,因为新生代的Java对象大多都是朝生夕死,所以Scavenge GC非常频繁,一般回收速度也比较快。当Eden空间不足以为对象分配内存时,会触发Scavenge GC。

一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Scavenge GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden去能尽快空闲出来。

当年轻代堆空间紧张时会被触发

相对于全收集而言,收集间隔较短

4.3.1.2 全收集器

Full GC,指发生在老年代的GC,出现了Full GC一般会伴随着至少一次的Minor GC(老年代的对象大部分是Scavenge GC过程中从新生代进入老年代),比如:分配担保失败。Full GC的速度一般会比Scavenge GC慢10倍以上。当老年代内存不足或者显式调用System.gc()方法时,会触发Full GC。

当老年代或者持久代堆空间满了,会触发全收集操作

可以使用System.gc()方法来显式的启动全收集

全收集一般根据堆大小的不同,需要的时间不尽相同,但一般会比较长。

4.3.1.3 垃圾回收器的常规匹配

image-20210124121604076

4.3.2 常见垃圾回收算法

4.3.2.1 引用计数(Reference Counting)

比较古老的回收算法。原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一个计数。垃圾回收时,只用收集计数为0的对象。此算法最致命的是无法处理循环引用的问题。

4.3.2.2 复制(Copying)

此算法把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。此算法每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理,不会出现“碎片”问题。当然,此算法的缺点也是很明显的,就是需要两倍内存空间。简图如下:

copy

4.3.2.3 标记-清除(Mark-Sweep)

此算法执行分两阶段。第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标记的对象清除。此算法需要暂停整个应用,同时,会产生内存碎片。简图如下:

marksweep

4.3.2.4 标记-整理(Mark-Compact)

此算法结合了“标记-清除”和“复制”两个算法的优点。也是分两阶段,第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,把清除未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。简图如下:

markcompact

4.3.3 分代垃圾收集器

4.3.3.1 串行收集器(Serial)

Serial收集器是Hotspot运行在Client模式下的默认新生代收集器, 它的特点是:只用一个CPU(计算核心)/一条收集线程去完成GC工作, 且在进行垃圾收集时必须暂停其他所有的工作线程(“Stop The World” -后面简称STW)。可以使用-XX:+UseSerialGC打开。 虽然是单线程收集, 但它却简单而高效, 在VM管理内存不大的情况下(收集几十M~一两百M的新生代), 停顿时间完全可以控制在几十毫秒~一百多毫秒内。

serial

4.3.3.2 并行收集器(ParNew)

ParNew收集器其实是前面Serial的多线程版本, 除使用多条线程进行GC外, 包括Serial可用的所有控制参数、收集算法、STW、对象分配规则、回收策略等都与Serial完全一样(也是VM启用CMS收集器-XX: +UseConcMarkSweepGC的默认新生代收集器)。

由于存在线程切换的开销, ParNew在单CPU的环境中比不上Serial, 且在通过超线程技术实现的两个CPU的环境中也不能100%保证能超越Serial. 但随着可用的CPU数量的增加, 收集效率肯定也会大大增加(ParNew收集线程数与CPU的数量相同, 因此在CPU数量过大的环境中, 可用-XX:ParallelGCThreads=<N>参数控制GC线程数)。

parnew

4.3.3.3 Parallel Scavenge收集器

与ParNew类似, Parallel Scavenge也是使用复制算法, 也是并行多线程收集器. 但与其他收集器关注尽可能缩短垃圾收集时间不同, Parallel Scavenge更关注系统吞吐量:

系统吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)

停顿时间越短就越适用于用户交互的程序-良好的响应速度能提升用户的体验;而高吞吐量则适用于后台运算而不需要太多交互的任务-可以最高效率地利用CPU时间,尽快地完成程序的运算任务. Parallel Scavenge提供了如下参数设置系统吞吐量:

Parallel Scavenge参数描述
-XX:MaxGCPauseMillis(毫秒数) 收集器将尽力保证内存回收花费的时间不超过设定值, 但如果太小将会导致GC的频率增加.
-XX:GCTimeRatio(整数:0 < GCTimeRatio < 100) 是垃圾收集时间占总时间的比率
XX:+UseAdaptiveSizePolicy启用GC自适应的调节策略: 不再需要手工指定-Xmn、-XX:SurvivorRatio、-XX:PretenureSizeThreshold等细节参数, VM会根据当前系统的运行情况收集性能监控信息, 动态调整这些参数以提供最合适的停顿时间或最大的吞吐量

4.3.3.4 Serial Old收集器

Serial Old是Serial收集器的老年代版本, 同样是单线程收集器,使用“标记-整理”算法。是一个单线程收集器

serialold

4.3.3.5 Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本, 使用多线程和“标记-整理”算法, 吞吐量优先, 主要与Parallel Scavenge配合在注重吞吐量及CPU资源敏感系统内使用;

parallelold

4.3.3.6 CMS收集器(Concurrent Mark Sweep)

CMS(Concurrent Mark Sweep)收集器是一款具有划时代意义的收集器, 一款真正意义上的并发收集器, 虽然现在已经有了理论意义上表现更好的G1收集器, 但现在主流互联网企业线上选用的仍是CMS(如Taobao、微店).

CMS是一种以获取最短回收停顿时间为目标的收集器(CMS又称多并发低暂停的收集器), 基于”标记-清除”算法实现, 整个GC过程分为以下4个步骤:

  1. 初始标记(CMS initial mark)
  2. 并发标记(CMS concurrent mark: GC Roots Tracing过程)
  3. 重新标记(CMS remark)
  4. 并发清除(CMS concurrent sweep: 已死对象将会就地释放, 注意:此处没有压缩) 其中1,3两个步骤(初始标记、重新标记)仍需STW. 但初始标记仅只标记一下GC Roots能直接关联到的对象, 速度很快; 而重新标记则是为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录, 虽然一般比初始标记阶段稍长, 但要远小于并发标记时间.

cms

CMS特点:

  1. CMS默认启动的回收线程数=(CPU数目+3)4 当CPU数>4时, GC线程一般占用不超过25%的CPU资源, 但是当CPU数<=4时, GC线程可能就会过多的占用用户CPU资源, 从而导致应用程序变慢, 总吞吐量降低.

  2. 无法处理浮动垃圾, 可能出现Promotion Failure、Concurrent Mode Failure而导致另一次Full GC的产生: 浮动垃圾是指在CMS并发清理阶段用户线程运行而产生的新垃圾. 由于在GC阶段用户线程还需运行, 因此还需要预留足够的内存空间给用户线程使用, 导致CMS不能像其他收集器那样等到老年代几乎填满了再进行收集. 因此CMS提供了-XX:CMSInitiatingOccupancyFraction参数来设置GC的触发百分比(以及-XX:+UseCMSInitiatingOccupancyOnly来启用该触发百分比), 当老年代的使用空间超过该比例后CMS就会被触发(JDK 1.6之后默认92%). 但当CMS运行期间预留的内存无法满足程序需要, 就会出现上述Promotion Failure等失败, 这时VM将启动后备预案: 临时启用Serial Old收集器来重新执行Full GC(CMS通常配合大内存使用, 一旦大内存转入串行的Serial GC, 那停顿的时间就是大家都不愿看到的了).

  3. 最后, 由于CMS采用”标记-清除”算法实现, 可能会产生大量内存碎片. 内存碎片过多可能会导致无法分配大对象而提前触发Full GC. 因此CMS提供了-XX:+UseCMSCompactAtFullCollection开关参数, 用于在Full GC后再执行一个碎片整理过程. 但内存整理是无法并发的, 内存碎片问题虽然没有了, 但停顿时间也因此变长了, 因此CMS还提供了另外一个参数-XX:CMSFullGCsBeforeCompaction用于设置在执行N次不进行内存整理的Full GC后, 跟着来一次带整理的(默认为0: 每次进入Full GC时都进行碎片整理).

4.3.3.7 分区收集- G1收集器

G1(Garbage-First)是一款面向服务端应用的收集器, 主要目标用于配备多颗CPU的服务器治理大内存.

  • G1 is planned as the long term replacement for the Concurrent Mark-Sweep Collector (CMS). -XX:+UseG1GC启用G1收集器. 与其他基于分代的收集器不同, G1将整个Java堆划分为多个大小相等的独立区域(Region), 虽然还保留有新生代和老年代的概念, 但新生代和老年代不再是物理隔离的了, 它们都是一部分Region(不需要连续)的集合.如:

g1

JVM参数
参数描述
-Xms设置JVM启动时初始化堆大小
-Xmx设置堆的最大值
-Xmn设置年轻代大小
-XX:PermGen设置老年代的初始化大小
-XX:MaxPermGen设置老年代的最大值
-XX:SurvivorRation设置Eden区和Survivor区的比例,默认为8
-XX:NewRatio设置老年代和年轻代的比例,默认为2
GC参数
参数描述
-XX:+UseSerialGCclient模式下默认值,打开此开关后,采用Serial+Serial Old组合进行垃圾回收
-XX:+UseParallelGC采用Parallel Scanvenge+Parallel Old
-XX:+UseParallelOldGCParallel Scanvenge + Parallel Old
-XX:+ParNewGCParNew + Serial Old
-XX:+UseConcMarkSweepGC采用ParNew+CMS+Serial Old
-XX:+UseG1GC采用G1收集器

每块区域既有可能属于O区、也有可能是Y区, 因此不需要一次就对整个老年代/新生代回收. 而是当线程并发寻找可回收的对象时, 有些区块包含可回收的对象要比其他区块多很多. 虽然在清理这些区块时G1仍然需要暂停应用线程, 但可以用相对较少的时间优先回收垃圾较多的Region. 这种方式保证了G1可以在有限的时间内获取尽可能高的收集效率.

G1的新生代收集跟ParNew类似: 存活的对象被转移到一个/多个Survivor Regions. 如果存活时间达到阀值, 这部分对象就会被提升到老年代.如图:

g1-1

g1-2

其特定是:

一整块堆内存被分为多个Regions.

存活对象被拷贝到新的Survivor区或老年代.

年轻代内存由一组不连续的heap区组成, 这种方法使得可以动态调整各代区域尺寸.

Young GC会有STW事件, 进行时所有应用程序线程都会被暂停.

多线程并发GC.

G1老年代GC特点如下:

并发标记阶段

1 在与应用程序并发执行的过程中会计算活跃度信息.

2 这些活跃度信息标识出那些regions最适合在STW期间回收(which regions

will be best to reclaim during an evacuation pause).

3 不像CMS有清理阶段.

再次标记阶段

1 使用Snapshot-at-the-Beginning(SATB)算法比CMS快得多.

2 空region直接被回收.

拷贝/清理阶段(Copying/Cleanup Phase)

1 年轻代与老年代同时回收.

2 老年代内存回收会基于他的活跃度信息.

4.4 JVM优化

4.4.1 JDK常用JVM优化相关命令

jdk给程序员提供的辅助工具都存放在bin目录下

bin描述功能
jps打印Hotspot VM进程VMID、JVM参数、main()函数参数、主类名/Jar路径
jstat查看Hotspot VM 运行时信息类加载、内存、GC[可分代查看]、JIT编译
命令格式:jstat -gc 10340 250 20
jinfo查看和修改虚拟机各项配置-flag name=value
jmapheapdump: 生成VM堆转储快照、查询finalize执行队列、Java堆和永久代详细信息jmap -dump:live,format=b,file=heap.bin [VMID]
jhat用于分析heapdump文件,它会建立一个HTTP/HTML服务器,让用户可以在浏览器上查看分析结果
jstack查看VM当前时刻的线程快照: 当前VM内每一条线程正在执行的方法堆栈集合Thread.getAllStackTraces()提供了类似的功能
javap查看经javac之后产生的JVM字节码代码自动解析.class文件, 避免了去理解class文件格式以及手动解析class文件内容
jcmd一个多功能工具, 可以用来导出堆, 查看Java进程、导出线程信息、 执行GC、查看性能相关数据等几乎集合了jps、jstat、jinfo、jmap、jstack所有功能
jconsole基于JMX的可视化监视、管理工具可以查看内存、线程、类、CPU信息, 以及对JMX MBean进行管理
jvisualvmJDK中最强大运行监视和故障处理工具可以监控内存泄露、跟踪垃圾回收、执行时内存分析、CPU分析、线程分析…
JprofilerJDK中运行监视和故障处理工具

4.4.1.1 jps

列出正在运行的虚拟机进程。并显示虚拟机执行主类(Main Class,main()函数所在的类)名称以及这些进程的本地虚拟机唯一ID(Local Virtual Machine Identifier,LVMID)

虽然功能比较单一,但它是使用频率最高的JDK命令行工具,因为其他的JDK工具大多需要输入它查询到的唯一ID来确定要监控的是哪一个虚拟机进程。对于本地虚拟机进程来说,唯一ID与操作系统的进程ID是一致的。使用Windows的任务管理器或者UNIX的ps命令也可以查询到虚拟机进程的唯一ID,但如果同时启动多个虚拟机进程,无法根据进程名称定位时,那只有依赖jps命令 显示主类的功能 才能区分了。

命令格式:

jps[options][hostid]

参数解释:

第一个参数:options

-q:显示进程ID

-m:显示进程ID,主类名称,以及传入main方法的参数

-l:显示进程ID,主类全名

-v:显示进程ID,主类名称,以及传入JVM的参数

-V:显示进程ID,主类名称

[-mlvV]可以任意组合使用

第二个参数:hostid

​ 主机或者是服务器的ip,如果不指定,就默认为当前的主机或者是服务器。

​ 注意:如果需要查看其他机器上的jvm进程,需要在待查看机器上启动jstatd。

示例

jps - l

显示线程id和执行线程的主类名

jps -v

显示线程id和执行线程的主类名和JVM配置信息

4.4.1.2 jstat

监视虚拟机各种运行状态信息,可以显示本地或者是远程虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据

命令格式:

jstat \[ options vmid \[ interval \[count\] \] \] <pid>

参数解释:

第一个参数:options

代表用户希望查询的虚拟机信息,主要分为3类:类装载、垃圾收集和运行期编译状况,具体选项及作用如下:

-class:显示有关类加载器行为的统计信息

-compiler:显示有关Java HotSpot VM即时编译器行为的统计信息

-gc:显示有关垃圾收集堆行为的统计信息

-gccapacity:显示有关各个垃圾回收代容量及其相应空间的统计信息

-gccause:显示有关垃圾收集统计信息(同-gcutil),以及上一次和当前(如果适用)垃圾收集事件的原因

-gcnew:显示新生代行为的统计信息

-gcnewcapacity:显示有关新生代大小及其相应空间的统计信息

-gcold:显示有关老年代行为的统计信息和元空间统计信息

-gcoldcapacity:显示有关老年代大小的统计信息

-gcmetacapacity:显示有关元空间大小的统计信息

-gcutil:显示有关垃圾收集统计信息

-printcompilation:显示Java HotSpot VM编译方法统计信息

第二个参数:vmid

如果是本地虚拟机进程,vmid和本地虚拟机唯一ID是一致的

如果是远程虚拟机进程,那vmid的格式应当是:

[protocol:][//]lvmid[@hostname[:port]/servername]

第三个参数:interval

采样间隔,单位为秒(s)或毫秒(ms)

默认单位是毫秒。必须为正整数。

指定后,该jstat命令将在每个间隔产生其输出

第四个参数:count

要显示的样本数

注意点:

参数interval和count代表查询间隔和次数,如果省略这两个参数,说明只查询一次:

jstat -参数 线程id 执行时间(单位毫秒) 执行次数

jstat -gc 4488 30 10

image-20210124123400615

常见统计选项:

-class选项:

类加载器统计信息:

Loaded:已加载的类数

Bytes:加载的kB数

Unloaded:卸载的类数

Bytes:卸载的KB数

Time:执行类加载和卸载操作所花费的时间

-compiler选项:

Java HotSpot VM即时编译器统计信息:

Compiled:执行的编译任务数

Failed:编译任务数失败

Invalid:无效的编译任务数

Time:执行编译任务所花费的时间

FailedType:上次失败的编译的编译类型

FailedMethod:上次失败的编译的类名和方法

-gc 选项:

垃圾收集的堆统计信息

S0C:当前幸存者空间0容量(kB)

S1C:当前生存空间1的容量(kB)

S0U:幸存者空间0使用大小(kB)

S1U:幸存者空间1使用大小(kB)

EC:当前伊甸园空间容量(kB)

EU:伊甸园空间使用大小(kB)

OC:当前的老年代容量(kB)

OU:老年代使用大小(kB)

MC:元空间容量(kB)

MU:元空间使用大小(kB)

CCSC:压缩的类空间容量(kB)

CCSU:使用的压缩类空间(kB)

YGC:新生代垃圾收集事件的数量

YGCT:新生代垃圾回收时间

FGC:完整GC事件的数量

FGCT:完整的垃圾收集时间

GCT:总垃圾收集时间

-gcutil 选项:

垃圾收集统计信息

S0:幸存者空间0利用率占该空间当前容量的百分比

S1:幸存者空间1利用率占空间当前容量的百分比

E:Eden空间利用率占空间当前容量的百分比

O:老年代利用率占空间当前容量的百分比

M:元空间利用率占空间当前容量的百分比

CCS:压缩的类空间利用率,以百分比表示

YGC:新生代GC事件的数量

YGCT:新生代垃圾回收时间

FGC:完整GC事件的数量

FGCT:完整的垃圾收集时间

GCT:总垃圾收集时间

4.4.1.3 jinfo

**作用:**实时地查看和调整虚拟机各项参数

命令格式:

jinfo [options] <pid>

参数解释:

第一个参数:options

no option:输出全部的参数和系统属性

-flag name:输出对应名称的参数

-flag [+|-]name:开启或者关闭对应名称的参数

-flag name=value:设定对应名称的参数

-flags:输出全部的参数

-sysprops:输出系统属性

命令演示:

命令:jinfo pid

描述:输出当前 jvm 进程的全部参数和系统属性

命令:jinfo -flag name pid

描述:使用该命令,可以查看指定的 jvm 参数的值

如:查看当前 jvm 进程是否开启打印 GC 日志

jinfo -flag PrintGC pid

命令:jinfo -flag [+|-]name pid

描述:开启或者关闭对应名称的参数

使用 jinfo 可以在不重启虚拟机的情况下,可以动态的修改 jvm 的参数。尤其在线上的环境特别有用。

jinfo -flag +PrintGC pid

jinfo -flag PrintGC pid

jinfo -flag +PrintGC pid

jinfo -flag PrintGC pid

命令:jinfo -flag name=value pid

描述:修改指定参数的值。

和上面的例子相似,但是上面的主要是针对 boolean 值的参数设置的。

如果是设置 value值,则需要使用 name=value 的形式

jinfo -flag HeapDumpPath pid

jinfo -flag HeapDumpPath=d:\dump pid

jinfo -flag HeapDumpPath pid

jinfo -flag HeapDumpPath= pid

jinfo -flag HeapDumpPath pid

**注意:**jinfo虽然可以在java程序运行时动态地修改虚拟机参数,但并不是所有的参数都支持动态修改

命令:jinfo -flags pid

描述:输出全部的虚拟机参数

命令:jinfo -sysprops pid

描述:输出当前虚拟机进程的全部的系统属性

4.4.1.4 jmap

**作用:**是一个多功能的命令,它可以生成 java 程序的 dump 文件, 也可以查看堆内对象信息、查看 ClassLoader 的信息以及 finalizer 队列

命令格式:

jmap [options] <pid>

参数解释:

第一个参数:options

no option: 查看进程的内存映像信息,类似 Solaris pmap 命令。

heap: 显示Java堆详细信息

histo[:live]: 显示堆中对象的统计信息

clstats:打印类加载器信息

finalizerinfo: 显示在F-Queue队列等待Finalizer线程执行finalizer方法的对象

dump:<dump-options>:生成堆转储快照

命令演示:

命令:jmap pid

描述:查看进程的内存映像信息

使用不带选项参数的jmap打印共享对象映射,将会打印目标虚拟机中加载的每个共享对象的起始地址、映射大小以及共享对象文件的路径全称

命令:jmap -heap pid

描述:显示Java堆详细信息

打印一个堆的摘要信息,包括使用的GC算法、堆配置信息和各内存区域内存使用信息

命令:jmap -histo:live pid

命令:jmap -histo pid

描述:显示堆中对象的统计信息

其中包括每个Java类、对象数量、内存大小(单位:字节)、完全限定的类名。打印的虚拟机内部的类名称将会带有一个’*’前缀。如果指定了live子选项,则只计算活动的对象

命令:jmap -clstats pid

描述:打印类加载器信息

-clstats是-permstat的替代方案,在JDK8之前,-permstat用来打印类加载器的数据

打印Java堆内存的永久保存区域的类加载器的智能统计信息。对于每个类加载器而言,它的名称、活跃度、地址、父类加载器、它所加载的类的数量和大小都会被打印。此外,包含的字符串数量和大小也会被打印。

命令:jmap -finalizerinfo pid

描述:打印等待终结的对象信息

Number of objects pending for finalization:0 说明当前F-Queue队列中并没有等待Finalizer线程执行finalizer方法的对象。

命令:jmap -dump:live,format=b,file=d:\jmap.bin pid

描述:生成堆转储快照dump文件

以hprof二进制格式转储Java堆到指定filename的文件中。live子选项是可选的。如果指定了live子选项,堆中只有活动的对象会被转储。想要浏览heap dump,你可以使用jhat(Java堆分析工具)读取生成的文件。这个命令执行,JVM会将整个heap的信息dump写入到一个文件,heap如果比较大的话,就会导致这个过程比较耗时,并且执行的过程中为了保证dump的信息是可靠的,所以会暂停应用, 线上系统慎用

4.4.1.5 jhat

**作用:**与jmap搭配使用来分析jmap生成的堆转储快照。jhat内置了一个微型的HTTP/HTML服务器,生成dump文件的分析结果后,可以在浏览器中查看。

不过,这个工具一般除非实在没有其他工具,否则不会使用。主要原因有二:一是一般不会在部署应用程序的服务器上直接分析dump文件,即使可以这样做,也会尽量将dump文件拷贝到其他机器上进行分析,因为分析工作是一个耗时且消耗硬件资源的过程,既然都要在其他机器上进行,就没必要受到命令行工具的限制了; 二是jhat的分析功能相对来说比较简陋,其他有较好的替代工具,比如VisualVM,以及专业用于分析dump文件的Eclipse Memory Analyzer、 IBM HeapAnalyzer等工具都能实现比jhat更强大更专业的分析功能。

命令格式:

jhat [options] 堆转储文件

参数解释:

第一个参数:options

[-stack <bool>]:开关对象分配调用栈跟踪(tracking object allocation call stack)。 如果分配位置信息在堆转储中不可用,则必须将此标志设置为 false,默认值为 true。

[-refs <bool>]:开关对象引用跟踪(tracking of references to objects)。 默认值为 true。默认情况下,返回的指针是指向其他特定对象的对象,如反向链接或输入引用(referrers or incoming references),会统计/计算堆中的所有对象。

[-port <port>]:设置 jhat HTTP server 的端口号。默认值 7000。

[-exclude <file>]:指定对象查询时需要排除的数据成员列表文件(列出应从可访问对象查询中排除的数据成员的文件)。例如,如果文件列列出了 java.lang.String.value,那么当从某个特定对象 Object o 计算可达的对象列表时,引用路径涉及 java.lang.String.value 的都会被排除

[-baseline <file>]:指定一个基准堆转储(baseline heap dump)。在两个 heap dumps 中有相同 object ID 的对象会被标记为不是新的(marked as not being new)。其他对象被标记为新的(new)。在比较两个不同的堆转储时很有用。

[-debug <int>]:设置 debug 级别。0 表示不输出调试信息。值越大则表示输出更详细的 debug 信息。

[-version]:启动后只显示版本信息就退出

第二个参数:堆转储文件

要浏览的Java二进制堆转储文件

命令演示:

命令:jhat D:\jmap.bin

执行命令后,我们看到系统开始读取这段dump信息,当系统提示Server is ready的时候,用户可以通过在浏览器键入http://ip:7000进行查询。有时 dump 文件很大,在启动时会报堆空间不足的错误,可加参数 jhat -J-Xmx512m <heap dump file>,这个内存大小可自行设置。

看文件中的内容,重点看下面的七个:

jhat 启动后显示的 html 页面中包含有:

A:显示出堆中所包含的所有的类(All Classes(including platform))。

B:从根集能引用到的对象(All Members of the Rootset)。

C:显示平台包括的所有类的实例数量(Instance Counts for All Classes(including platform/excluding platform))。

D:一般查看堆异常情况主要看 Instance Counts for All Classes(excluding platform)平台外的所有对象信息和 Heap Histogram 以树状图形式展示堆情况。

E:堆实例的分布表(Heap Histogram)。

F:Finalizer 摘要(Finalizer Summary)。

G:执行对象查询语句(OQL)。

#查询字符串

select s from java.lang.String s

4.4.1.6 jstack

**作用:**查看或导出 Java 应用程序中线程堆栈信息 .

线程快照是当前java虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、长时间等待外部资源等。 线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。 如果java程序崩溃生成core文件,jstack工具可以用来获得core文件的java stack和native stack的信息,从而可以轻松地知道java程序是如何崩溃和在程序何处发生问题。另外,jstack工具还可以附属到正在运行的java程序中,看到当时运行的java程序的java stack和native stack的信息.

命令格式:

jstack [ options ] <pid>

参数解释:

第一个参数:options

-F : 当线程挂起时,使用jstack -l pid 请求不被响应时,强制输出线程堆栈

-l : 除堆栈外,显示关于锁的附加信息,例如 ownable synchronizers

-m : 可以同时输出java以及C/C++的堆栈信息

演示:

​ CPU占用过高:

​ 1.使用Process Explorer工具,找到CPU占用率高的进程的id;

​ 2.右击该进程,查看属性,在thread选项卡中,找到cpu占用率高的线程id

​ 3.把线程id转换成16进制

​ 4.使用jstack -l <pid> 查看进程的线程快照

​ 5.在线程快照中找到指定的线程,并分析代码

4.4.1.7 常用jdk工具

JConsole

**作用:**查看Java应用程序的运行概况,监视垃圾收集器管理的虚拟机内存(堆和元空间)的变化趋势,以及监控程序内的线程。

启动JConsole,在控制台输入:jconsole即可,在弹出的界面中,选择本地进程,然后进去看界面页签信息。显示的是整个虚拟机主要运行数据的概览,其中包括堆内存使用情况,线程,类,CPU使用情况四项信息的曲线图。

内存:

相当于命令行的jstat命令,用于监视受垃圾收集器管理的虚拟机内存(堆和元空间)的变化趋势,这不仅是包括堆内存的整体信息,更细化到伊甸区、幸存区、老年代的使用情况。同时,也包括非堆区,即元空间的使用情况,单机界面右上角的“执行GC”按钮,可以强制应用程序进行一次Full GC。

线程:

相当于命令行的jstack命令,遇到线程停顿的时候可以使用它来进行监控分析。JConsole 显示了系统内的线程数量,并在屏幕下方,显示了程序中所有的线程。单击线程名称,便可以查看线程的栈信息。

类:

如图所示,显示了系统以及装载的类数量。在详细信息栏中,还显示了已卸载的类数量。

VM摘要:

在VM摘要页面,JConsole 显示了当前应用程序的运行环境。包括虚拟机类型、版本、堆信息以及虚拟机参数等。相当于jinfo命令

MBean:

MBean页面允许通过JConsole访问已经在MBean服务器注册的MBean对象。

jvisualvm

**作用:**是到目前为止随JDK发布的功能最强大的运行监视和故障处理程序,并且可以遇见在未来一段时间内都是官方主力发展的虚拟机故障处理工具。官方在VisualVM的软件说明中写上了“All-in-One”的描述字样,预示着他除了运行监视、故障处理外,还提供了很多其他方面的功能。如性能分析,VisualVM的性能分析功能甚至比起很多专业的收费的工具都不会逊色多少,而且VisualVM还有一个很大的优点:不需要被监视的程序基于特殊的运行,因此他对应用程序的实际性能的影响很小,使得他可以直接应用在生产环境中。

VisualVM基于NetBeans平台开发,因此他一开始就具备了插件扩展功能的特性,通过插件扩展支持,VisualVM可以做到:

  • 显示虚拟机进程以及进程的配置、环境信息(jps、jinfo)。
  • 监视应用程序的CPU、GC、堆、方法区以及线程的信息(jstat、jstack)。
  • dump以及分析堆转储快照(jmap、jhat)。
  • 方法级的程序运行性能分析,找到被调用最多、运行时间最长的方法。
  • 离线程序快照:收集程序的运行时配置、线程dump、内存dump等信息建立一个快照,可以将快照发送开发者处进行Bug反馈。
  • 其他plugins的无限的可能性......

使用:

1.在控制台输入:jvisualvm执行即可;

2.安装插件:

​ 2.1 从主菜单中选择“工具”>“插件” ;

​ 2.2 在“可用插件”标签中,选中该插件的“安装”复选框。单击“安装” ;

​ 2.3 逐步完成插件安装程序。

默认插件:

概述(Overview):

​ 程序的基本信息和启动参数、环境变量等等;

监视(Monitor):

​ 用于显示CPU、内存(分为Heap和Metaspace)、类和线程的使用情况或者数量,另外还包括执行垃圾回收和对堆 Dump的快捷功能 ;

线程(Threads):

​ 详细查看每个线程的运行时间及状态等;

抽样器(Sampler):

​ 对CPU和内存进行一段时长的取样,从而对应用程序进行分析 ;

4.4.2 内存分析和线程分析

监视:

​ 监视是一种用来查看应用程序运行时行为的一般方法。通常会有多个视图(View)分别实时地显示 CPU 使用情况、内存使用情况、线程状态以及其他一些有用的信息,以便用户能很快地发现问题的关键所在。

转储:

​ 性能分析工具从内存中获得当前状态数据并存储到文件用于静态的性能分析。Java 程序是通过在启动 Java 程序时添加适当的条件参数来触发转储操作的。它包括以下三种:

  • ​ 系统转储:JVM 生成的本地系统的转储,又称作核心转储。一般的,系统转储数据量大,需要平台相关的工具去分析,如 Windows 上的 windbg 和 Linux 上的 gdb。

  • ​ Java 转储:JVM 内部生成的格式化后的数据,包括线程信息,类的加载信息以及堆的统计数据。通常也用于检测死锁。

  • ​ 堆转储:JVM 将所有对象的堆内容存储到文件。

快照:

​ 应用程序启动后,性能分析工具开始收集各种运行时数据,其中一些数据直接显示在监视视图中,而另外大部分数据被保存在内部,直到用户要求获取快照,基于这些保存的数据的统计信息才被显示出来。快照包含了应用程序在一段时间内的执行信息,通常有 CPU 快照和内存快照两种类型。

  • ​ CPU 快照:主要包含了应用程序中函数的调用关系及运行时间,这些信息通常可以在 CPU 快照视图中进行查看。

  • ​ 内存快照:主要包含了内存的分配和使用情况、载入的所有类、存在的对象信息及对象间的引用关系等。这些信息通常可以在内存快照视图中进行查看。

性能分析:

​ 性能分析是通过收集程序运行时的执行数据来帮助开发人员定位程序需要被优化的部分,从而提高程序的运行速度或是内存使用效率,主要有以下三个方面:

  • ​ CPU 性能分析:CPU 性能分析的主要目的是统计函数的调用情况及执行时间,或者更简单的情况就是统计应用程序的 CPU 使用情况。通常有 CPU 监视和 CPU 快照两种方式来显示 CPU 性能分析结果。

  • ​ 内存性能分析:内存性能分析的主要目的是通过统计内存使用情况检测可能存在的内存泄露问题及确定优化内存使用的方向。通常有内存监视和内存快照两种方式来显示内存性能分析结果。

  • ​ 线程性能分析:线程性能分析主要用于在多线程应用程序中确定内存的问题所在。一般包括线程的状态变化情况,死锁情况和某个线程在线程生命期内状态的分布情况等

4.4.3 JVM常见参数

配置方式:java [options] MainClass [arguments]

options - JVM启动参数。 配置多个参数的时候,参数之间使用空格分隔。

参数命名: 常见为 -参数名

参数赋值: 常见为 -参数名=参数值 | -参数名:参数值

4.4.3.1 内存设置

-Xms:初始堆大小,JVM启动的时候,给定堆空间大小。

-Xmx:最大堆大小,JVM运行过程中,如果初始堆空间不足的时候,最大可以扩展到多少。

-Xmn:设置年轻代大小。整个堆大小=年轻代大小+年老代大小+持久代大小。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。

-Xss: 设置每个线程的Java栈大小。JDK5.0以后每个线程Java栈大小为1M,以前每个线程堆栈大小为256K。根据应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。

-XX:NewSize=n:设置年轻代大小

-XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代+年老代和的1/4

-XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5

-XX:MaxPermSize=n:设置持久代大小

-XX:MaxTenuringThreshold:设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概率。

4.4.3.2 内存设置经验分享

JVM中最大堆大小有三方面限制:相关操作系统的数据模型(32-bt还是64-bit)限制;系统的可用虚拟内存限制;系统的可用物理内存限制。32位系统 下,一般限制在1.5G~2G;64为操作系统对内存无限制。

Tomcat配置方式: 编写catalina.bat|catalina.sh,增加JAVA_OPTS参数设置。windows和linux配置方式不同。windows - set "JAVA_OPTS=%JAVA_OPTS% 自定义参数";linux - JAVA_OPTS="$JAVA_OPTS 自定义参数"

常见设置:

-Xmx3550m -Xms3550m -Xmn2g -Xss128k 适合开发过程的测试应用。要求物理内存大于4G。

-Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=160m -XX:MaxTenuringThreshold=0 适合高并发本地测试使用。且大数据对象相对较多(如IO流)

环境: 16G物理内存,高并发服务,重量级对象中等(线程池,连接池等),常用对象比例为40%(运行过程中产生的对象40%是生命周期较长的)

-Xmx10G -Xms10G -Xss1M -XX:NewRatio=3 -XX:SurvivorRatio=4 -XX:MaxPermSize=2048m -XX:MaxTenuringThreshold=5

4.4.3.3 收集器设置

收集器配置的时候,次收集器和全收集器必须匹配。具体匹配规则参考3.1.3

-XX:+UseSerialGC:设置串行收集器,年轻带收集器, 次收集器

-XX:+UseParallelGC:设置并行收集器

-XX:+UseParNewGC:设置年轻代为并行收集。可与CMS收集同时使用。JDK5.0以上,JVM会根据系统配置自行设置,所以无需再设置此值。

-XX:+UseParallelOldGC:设置并行年老代收集器,JDK6.0支持对年老代并行收集。

-XX:+UseConcMarkSweepGC:设置年老代并发收集器,测试中配置这个以后,-XX:NewRatio的配置失效,原因不明。所以,此时年轻代大小最好用-Xmn设置。

-XX:+UseG1GC:设置G1收集器

4.4.3.4 垃圾回收统计信息

类似日志的配置信息。会有控制台相关信息输出。 商业项目上线的时候,不允许使用。一定使用loggc

-XX:+PrintGC

-XX:+Printetails

-XX:+PrintGCTimeStamps

-Xloggc:filename

4.4.3.5 并行收集器设置

-XX:ParallelGCThreads=n:设置并行收集器收集时最大线程数使用的CPU数。并行收集线程数。

-XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间,单位毫秒。可以减少STW时间。

-XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)并发收集器设置

-XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。

-XX:+UseAdaptiveSizePolicy:设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低相应时间或者收集频率等,此值建议使用并行收集器时,一直打开。

-XX:CMSFullGCsBeforeCompaction=n:由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩、整理。

-XX:+UseCMSCompactAtFullCollection:打开对年老代的压缩。可能会影响性能,但是可以消除碎片

4.4.3.6 收集器设置经验分享

关于收集器的选择JVM给了三种选择:串行收集器、并行收集器、并发收集器,但是串行收集器只适用于小数据量的情况,所以这里的选择主要针对并行收集器和并发收集器。默认情况下,JDK5.0以前都是使用串行收集器,如果想使用其他收集器需要在启动时加入相应参数。JDK5.0以后,JVM会根据当前系统配置进行判断。

常见配置:

并行收集器主要以到达一定的吞吐量为目标,适用于科学计算和后台处理等。

-Xmx3800m -Xms3800m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20

使用ParallelGC作为并行收集器, GC线程为20(CPU核心数>=20时),内存问题根据硬件配置具体提供。建议使用物理内存的80%左右作为JVM内存容量。

-Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20 -XX:+UseParallelOldGC

指定老年代收集器,在JDK5.0之后的版本,ParallelGC对应的全收集器就是ParallelOldGC。可以忽略

-Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:MaxGCPauseMillis=100

指定GC时最大暂停时间。单位是毫秒。每次GC最长使用100毫秒。可以尽可能提高工作线程的执行资源。

-Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:MaxGCPauseMillis=100 -XX:+UseAdaptiveSizePolicy

UseAdaptiveSizePolicy是提高年轻代GC效率的配置。次收集器执行效率。

并发收集器主要是保证系统的响应时间,减少垃圾收集时的停顿时间。适用于应用服务器、电信领域、互联网领域等。

-Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC

指定年轻代收集器为ParNew,年老代收集器ConcurrentMarkSweep,并发GC线程数为20(CPU核心>=20),并发GC的线程数建议使用(CPU核心数+3)/4或CPU核心数【不推荐使用】。

-Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseConcMarkSweepGC -XX:CMSFullGCsBeforeCompaction=5 -XX:+UseCMSCompactAtFullCollection

CMSFullGCsBeforeCompaction=5执行5次GC后,运行一次内存的整理。

UseCMSCompactAtFullCollection执行老年代内存整理。可以避免内存碎片,提高GC过程中的效率,减少停顿时间。

4.4.3.7 简单总结

年轻代大小选择

响应时间优先的应用:尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达年老代的对象。

吞吐量优先的应用:尽可能的设置大,可能到达Gbit的程度。因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用。

年老代大小选择

响应时间优先的应用:年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数。如果堆设置小了,可以会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。最优化的方案,一般需要参考以下数据获得:

并发垃圾收集信息

持久代并发收集次数

传统GC信息

花在年轻代和年老代回收上的时间比例

减少年轻代和年老代花费的时间,一般会提高应用的效率

吞吐量优先的应用:一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代。原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代存放长期存活对象。

较小堆引起的碎片问题,因为年老代的并发收集器使用标记、清除算法,所以不会对堆进行压缩。当收集器回收时,他会把相邻的空间进行合并,这样可以分配给较大的对象。但是,当堆空间较小时,运行一段时间以后,就会出现“碎片”,如果并发收集器找不到足够的空间,那么并发收集器将会停止,然后使用传统的标记、整理方式进行回收。如果出现“碎片”,可能需要进行如下配置:

-XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩。

-XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次Full GC后,对年老代进行压缩

4.4.3.8 测试代码

java
package jvm;
import java.io.IOException;
import java.lang.management.GarbageCollectorMXBean;
import java.lang.management.ManagementFactory;
import java.util.List;
public class Test {
	public static void main(String[] args) {
		List<GarbageCollectorMXBean> l = ManagementFactory.getGarbageCollectorMXBeans();
        for(GarbageCollectorMXBean b : l) {
			System.out.println(b.getName());
		}
		try {
			System.in.read();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

五、Java对象的布局

术语参考: http://openjdk.java.net/groups/hotspot/docs/HotSpotGlossary.html

在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。

对象头

当一个线程尝试访问synchronized修饰的代码块时,它首先要获得锁,那么这个锁到底存在哪里呢?是存在锁对象的对象头中的。

HotSpot采用instanceOopDesc和arrayOopDesc来描述对象头,arrayOopDesc对象用来描述数组类型。instanceOopDesc的定义的在Hotspot源码的 instanceOop.hpp 文件中,另外,arrayOopDesc的定义对应 arrayOop.hpp 。

从instanceOopDesc代码中可以看到 instanceOopDesc继承自oopDesc,oopDesc的定义载Hotspot源码中的 oop.hpp 文件中

在普通实例对象中,oopDesc的定义包含两个成员,分别是 _mark 和 _metadata mark 表示对象标记、属于markOop类型,也就是接下来要讲解的Mark World,它记录了对象和锁有关的信息

metadata 表示类元信息,类元信息存储的是对象指向它的类元数据(Klass)的首地址,其中Klass表示普通指针、 _compressed_klass 表示压缩类指针。对象头由两部分组成,一部分用于存储自身的运行时数据,称之为 Mark Word,另外一部分是类型指针,及对象指向它的类元数据的指针。

Mark Word

Mark Word用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,占用内存大小与虚拟机位长一致。Mark Word对应的类型是 markOop 。源码位于 markOop.hpp 中。

在64位虚拟机下,Mark Word是64bit大小的

在32位虚拟机下,Mark Word是32bit大小的

klass pointer

这一部分用于存储对象的类型指针,该指针指向它的类元数据,JVM通过这个指针确定对象是哪个类的实例。该指针的位长度为JVM的一个字大小,即32位的JVM为32位,64位的JVM为64位。 如果应用的对象过多,使用64位的指针将浪费大量内存,统计而言,64位的JVM将会比32位的JVM多耗费50%的内存。为了节约内存可以使用选项 -XX:+UseCompressedOops 开启指针压缩,其中,oop即ordinary object pointer普通对象指针。开启该选项后,下列指针将压缩至32位:

  1. 每个Class的属性指针(即静态变量)
  2. 每个对象的属性指针(即对象变量)
  3. 普通对象数组的每个元素指针

当然,也不是所有的指针都会压缩,一些特殊类型的指针JVM不会优化,比如指向PermGen的Class对象指针(JDK8中指向元空间的Class对象指针)、本地变量、堆栈元素、入参、返回值和NULL指针等。对象头 = Mark Word + 类型指针(未开启指针压缩的情况下)

在32位系统中,Mark Word = 4 bytes,类型指针 = 4bytes,对象头 = 8 bytes = 64 bits;

在64位系统中,Mark Word = 8 bytes,类型指针 = 8bytes,对象头 = 16 bytes = 128bits;

**实例数据 ** 就是类中定义的成员变量。

对齐填充 对齐填充并不是必然存在的,也没有什么特别的意义,他仅仅起着占位符的作用,由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说,就是对象的大小必须是8字节的整数倍。而对象头正好是8字节的倍数,因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

六、模式应用

同步模式之保护性暂停

即 Guarded Suspension,用在一个线程等待另一个线程的执行结果

有一个结果需要从一个线程传递到另一个线程,让他们关联同一个 GuardedObject

如果有结果不断从一个线程到另一个线程那么可以使用消息队列(见生产者/消费者)

JDK 中,join 的实现、Future 的实现,采用的就是此模式

因为要等待另一方的结果,因此归类到同步模式

实现

java
class GuardedObject {

    private Object response;
    private final Object lock = new Object();

    public Object get() {
        synchronized (lock) {
            // 条件不满足则等待
            while (response == null) {
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            return response;
        }
    }

    public void complete(Object response) {
        synchronized (lock) {
            // 条件满足,通知等待线程
            this.response = response;
            lock.notifyAll();
        }
    }
}

应用

java
@Slf4j(topic = "c.TestGuardedObject")
public class TestGuardedObject {
    public static void main(String[] args) {
        GuardedObject guardedObject = new GuardedObject();
        new Thread(() -> {
            try {
                List<String> response = download();
                log.debug("download complete...");
                guardedObject.complete(response);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }).start();

        log.debug("waiting...");
        Object response = guardedObject.get();
        log.debug("get response: [{}] lines", ((List<String>) response).size());

    }

}

带超时版 GuardedObject

java
package cn.itcast.pattern;

import lombok.extern.slf4j.Slf4j;

import java.util.Arrays;
import java.util.List;

import static cn.itcast.n2.util.Sleeper.sleep;

@Slf4j(topic = "c.TestGuardedObjectV2")
public class TestGuardedObjectV2 {
    public static void main(String[] args) {
        GuardedObjectV2 v2 = new GuardedObjectV2();
        new Thread(() -> {
            sleep(1);
            v2.complete(null);
            sleep(1);
            v2.complete(Arrays.asList("a", "b", "c"));
        }).start();

        Object response = v2.get(2500);
        if (response != null) {
            log.debug("get response: [{}] lines", ((List<String>) response).size());
        } else {
            log.debug("can't get response");
        }
    }
}


/**
 * 添加超时处理
 */
@Slf4j(topic = "c.GuardedObjectV2")
class GuardedObjectV2 {

    private Object response;
    private final Object lock = new Object();

    public Object get(long millis) {
        synchronized (lock) {
            // 1) 记录最初时间
            long last = System.currentTimeMillis();
            // 2) 已经经历的时间
            long timePassed = 0;
            while (response == null) {
                // 4) 假设 millis 是 1000,结果在 400 时唤醒了,那么还有 600 要等
                long waitTime = millis - timePassed;
                log.debug("waitTime: {}", waitTime);
                if (waitTime <= 0) {
                    log.debug("break...");
                    break;
                }
                try {
                    lock.wait(waitTime);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 3) 如果提前被唤醒,这时已经经历的时间假设为 400
                timePassed = System.currentTimeMillis() - last;
                log.debug("timePassed: {}, object is null {}", timePassed, response == null);
            }
            return response;
        }
    }

    public void complete(Object response) {
        synchronized (lock) {
            // 条件满足,通知等待线程
            this.response = response;
            log.debug("notify...");
            lock.notifyAll();
        }
    }
}

多任务版 GuardedObject

如果需要在多个类之间使用 GuardedObject 对象,作为参数传递不是很方便,因此设计一个用来解耦的中间类,这样不仅能够解耦【结果等待者】和【结果生产者】,还能够同时支持多个任务的管理

java
package cn.itcast.pattern;

import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

import static cn.itcast.pattern.Downloader.download;

@Slf4j(topic = "c.TestGuardedObjectV3")
public class TestGuardedObjectV3 {
    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            GuardedObjectV3 v3 = Fetures.createFeture();

            new Thread(() -> {
                log.debug("waiting id({})...", v3.getId());
                log.debug("get response id({}): [{}] lines", v3.getId(), ((List<String>) v3.get()).size());
            }).start();

            new Thread(() -> {
                try {
                    List<String> lines = download();
                    log.debug("download complete id({})...", v3.getId());
                    Fetures.complete(v3.getId(), lines);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }).start();

        }
    }
}

class Fetures {
    private static final ConcurrentHashMap<Integer, GuardedObjectV3> FETURES = new ConcurrentHashMap<>();
    private static final AtomicInteger ID_GENERATOR = new AtomicInteger();

    public static GuardedObjectV3 createFeture() {
        // 为每个 GuardedObject 分配一个 id
        int id = ID_GENERATOR.incrementAndGet();
        GuardedObjectV3 v3 = new GuardedObjectV3(id);
        // 放入公共位置,将来异步响应返回时,根据编号获取
        FETURES.put(id, v3);
        return v3;
    }

    public static void complete(int id, Object response) {
        // 异步响应完成,根据编号获取并移除
        GuardedObjectV3 v3 = FETURES.remove(id);
        if (v3 != null) {
            v3.complete(response);
        }
    }
}


/**
 * 添加多任务处理
 */
class GuardedObjectV3 {

    private int id;
    private Object response;
    private final Object lock = new Object();


    public GuardedObjectV3(int id) {
        this.id = id;
    }

    public int getId() {
        return id;
    }

    public Object get() {
        synchronized (lock) {
            while (response == null) {
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            return response;
        }
    }

    public void complete(Object response) {
        synchronized (lock) {
            this.response = response;
            lock.notifyAll();
        }
    }
}

同步模式之 Balking

Balking (犹豫)模式用在一个线程发现另一个线程或本线程已经做了某一件相同的事,那么本线程就无需再做了,直接结束返回

实现

java
package cn.itcast.monitor.service;

import cn.itcast.monitor.controller.MonitorController;
import cn.itcast.monitor.vo.Info;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

/**
 * @author yihang
 */
@Service
@Slf4j
public class MonitorService {

    private volatile boolean stop;
    private volatile boolean starting;
    private Thread monitorThread;

    public void start() {
        // 缩小同步范围,提升性能
        synchronized (this) {
            log.info("该监控线程已启动?({})", starting);
            if (starting) {
                return;
            }
            starting = true;
        }

        // 由于之前的 balking 模式,以下代码只可能被一个线程执行,因此无需互斥
        monitorThread = new Thread(() -> {
            while (!stop) {
                report();
                sleep(2);
            }
            // 这里的监控线程只可能启动一个,因此只需要用 volatile 保证 starting 的可见性
            log.info("监控线程已停止...");
            starting = false;
        });

        stop = false;
        log.info("监控线程已启动...");
        monitorThread.start();
    }

    private void report() {
        Info info = new Info();
        info.setTotal(Runtime.getRuntime().totalMemory());
        info.setFree(Runtime.getRuntime().freeMemory());
        info.setMax(Runtime.getRuntime().maxMemory());
        info.setTime(System.currentTimeMillis());
        MonitorController.QUEUE.offer(info);
    }

    private void sleep(long seconds) {
        try {
            TimeUnit.SECONDS.sleep(seconds);
        } catch (InterruptedException e) {
        }
    }

    public synchronized void stop() {
        stop = true;
        // 不加打断需要等到下一次 sleep 结束才能退出循环,这里是为了更快结束
        monitorThread.interrupt();
    }

}

实现单例

java
package cn.itcast.n5;

public final class Singleton {
    private Singleton() {
    }

    private static volatile Singleton INSTANCE = null;

    public static Singleton getInstance() {
        // 实例没创建,才会进入内部的 synchronized代码块
        /*if (INSTANCE == null) {
            synchronized (Singleton.class) {
                // 也许有其它线程已经创建实例,所以再判断一次
                if (INSTANCE == null) {
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;*/


        if (INSTANCE != null) {
            return INSTANCE;
        }
        synchronized (Singleton.class) {
            // 也许有其它线程已经创建实例,所以再判断一次
            if (INSTANCE == null) {
                INSTANCE = new Singleton();
            }
            return INSTANCE;
        }
    }
}

同步模式之顺序控制

固定运行顺序

wait notify 版

实现较麻烦

首先,需要保证先 wait 再 notify,否则 wait 线程永远得不到唤醒。因此使用了『运行标记』来判断该不该wait

第二,如果有些干扰线程错误地 notify 了 wait 线程,条件不满足时还要重新等待,使用了 while 循环来解决此问题

最后,唤醒对象上的 wait 线程需要使用 notifyAll,因为『同步对象』上的等待线程可能不止一个

java
// 用来同步的对象
static Object obj = new Object();
// t2 运行标记, 代表 t2 是否执行过
static boolean t2runed = false;
public static void main(String[] args) {
 Thread t1 = new Thread(() -> {
 synchronized (obj) {
 // 如果 t2 没有执行过
 while (!t2runed) { 
 try {
 // t1 先等一会
 obj.wait(); 
 } catch (InterruptedException e) {
 e.printStackTrace();
      }
 }
 }
 System.out.println(1);
 });
 Thread t2 = new Thread(() -> {
 System.out.println(2);
 synchronized (obj) {
 // 修改运行标记
 t2runed = true;
 // 通知 obj 上等待的线程(可能有多个,因此需要用 notifyAll)
 obj.notifyAll();
 }
 });
 t1.start();
 t2.start();
}

Park Unpark 版

park 和 unpark 方法比较灵活,他俩谁先调用,谁后调用无所谓。并且是以线程为单位进行『暂停』和『恢复』,不需要『同步对象』和『运行标记』

java
Thread t1 = new Thread(() -> {
 try { Thread.sleep(1000); } catch (InterruptedException e) { }
 // 当没有『许可』时,当前线程暂停运行;有『许可』时,用掉这个『许可』,当前线程恢复运行
 LockSupport.park();
 System.out.println("1");
});
Thread t2 = new Thread(() -> {
 System.out.println("2");
 // 给线程 t1 发放『许可』(多次连续调用 unpark 只会发放一个『许可』)
 LockSupport.unpark(t1);
});
t1.start();
t2.start();

交替输出

线程 1 输出 a 5 次,线程 2 输出 b 5 次,线程 3 输出 c 5 次。现在要求输出 abcabcabcabcabc 怎么实现

wait notify 版

java
class SyncWaitNotify {
 private int flag;
 private int loopNumber;
 public SyncWaitNotify(int flag, int loopNumber) {
 this.flag = flag;
 this.loopNumber = loopNumber;
 }
 public void print(int waitFlag, int nextFlag, String str) {
 for (int i = 0; i < loopNumber; i++) {
 synchronized (this) {
 while (this.flag != waitFlag) {
 try {
 this.wait();
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
 }
 System.out.print(str);
 flag = nextFlag;
 this.notifyAll();
 }
 }
 }
}
java
SyncWaitNotify syncWaitNotify = new SyncWaitNotify(1, 5);
new Thread(() -> {
 syncWaitNotify.print(1, 2, "a");
}).start();
new Thread(() -> {
 syncWaitNotify.print(2, 3, "b");
}).start();
new Thread(() -> {
 syncWaitNotify.print(3, 1, "c");
}).start();

Lock 条件变量版

java
class AwaitSignal extends ReentrantLock {
 public void start(Condition first) {
 this.lock();
     try {
 log.debug("start");
 first.signal();
 } finally {
 this.unlock();
 }
 }
 public void print(String str, Condition current, Condition next) {
 for (int i = 0; i < loopNumber; i++) {
 this.lock();
 try {
 current.await();
 log.debug(str);
 next.signal();
 } catch (InterruptedException e) {
 e.printStackTrace();
 } finally {
 this.unlock();
 }
 }
 }
 // 循环次数
 private int loopNumber;
 public AwaitSignal(int loopNumber) {
 this.loopNumber = loopNumber;
 }
}
java
AwaitSignal as = new AwaitSignal(5);
Condition aWaitSet = as.newCondition();
Condition bWaitSet = as.newCondition();
Condition cWaitSet = as.newCondition();
new Thread(() -> {
 as.print("a", aWaitSet, bWaitSet);
}).start();
new Thread(() -> {
 as.print("b", bWaitSet, cWaitSet);
}).start();
new Thread(() -> {
 as.print("c", cWaitSet, aWaitSet);
}).start();
as.start(aWaitSet);

Park Unpark 版

java
class SyncPark {
 private int loopNumber;
 private Thread[] threads;
 public SyncPark(int loopNumber) {
 this.loopNumber = loopNumber;
 }
 public void setThreads(Thread... threads) {
 this.threads = threads;
 }
 public void print(String str) {
 for (int i = 0; i < loopNumber; i++) {
 LockSupport.park();
 System.out.print(str);
 LockSupport.unpark(nextThread());
 }
 }
 private Thread nextThread() {
 Thread current = Thread.currentThread();
 int index = 0;
 for (int i = 0; i < threads.length; i++) {
 if(threads[i] == current) {
 index = i;
 break;
 }
 }
 if(index < threads.length - 1) {
 return threads[index+1];
 } else {
 return threads[0];
 }
 }
 public void start() {
 for (Thread thread : threads) {
 thread.start();
 }
 LockSupport.unpark(threads[0]);
 }
}
java
SyncPark syncPark = new SyncPark(5);
Thread t1 = new Thread(() -> {
 syncPark.print("a");
});
Thread t2 = new Thread(() -> {
 syncPark.print("b");
});
Thread t3 = new Thread(() -> {
 syncPark.print("c\n");
});
syncPark.setThreads(t1, t2, t3);
syncPark.start();

异步模式之生产者/消费者

与前面的保护性暂停中的 GuardObject 不同,不需要产生结果和消费结果的线程一一对应

消费队列可以用来平衡生产和消费的线程资源

生产者仅负责产生结果数据,不关心数据该如何处理,而消费者专心处理结果数据

消息队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据

JDK 中各种阻塞队列,采用的就是这种模式

实现

java
package cn.itcast.pattern;


import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.util.LinkedList;
import java.util.List;

@Slf4j(topic = "c.TestProducerConsumer")
public class TestProducerConsumer {
    public static void main(String[] args) {
        MessageQueue messageQueue = new MessageQueue(2);
        for (int i = 0; i < 4; i++) {
            int id = i;
            new Thread(() -> {
                try {
                    log.debug("download...");
                    List<String> response = Downloader.download();
                    log.debug("try put message({})", id);
                    messageQueue.put(new Message(id, response));
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }, "生产者" + i).start();
        }

        new Thread(() -> {
            while (true) {
                Message message = messageQueue.take();
                List<String> response = (List<String>) message.getMessage();
                log.debug("take message({}): [{}] lines", message.getId(), response.size());
            }

        }, "消费者").start();
    }
}

class Message {
    private int id;
    private Object message;

    public Message(int id, Object message) {
        this.id = id;
        this.message = message;
    }

    public int getId() {
        return id;
    }

    public Object getMessage() {
        return message;
    }
}

@Slf4j(topic = "c.MessageQueue")
class MessageQueue {
    private LinkedList<Message> queue;
    private int capacity;

    public MessageQueue(int capacity) {
        this.capacity = capacity;
        queue = new LinkedList<>();
    }

    public Message take() {
        synchronized (queue) {
            while (queue.isEmpty()) {
                log.debug("没货了, wait");
                try {
                    queue.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            Message message = queue.removeFirst();
            queue.notifyAll();
            return message;
        }
    }

    public void put(Message message) {
        synchronized (queue) {
            while (queue.size() == capacity) {
                log.debug("库存已达上限, wait");
                try {
                    queue.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            queue.addLast(message);
            queue.notifyAll();
        }
    }
}

异步模式之工作线程

让有限的工作线程(Worker Thread)来轮流异步处理无限多的任务。也可以将其归类为分工模式,它的典型实现就是线程池,也体现了经典设计模式中的享元模式。

注意,不同任务类型应该使用不同的线程池,这样能够避免饥饿,并能提升效率

饥饿

固定大小线程池会有饥饿现象

两个工人是同一个线程池中的两个线程

他们要做的事情是:为客人点餐和到后厨做菜,这是两个阶段的工作

客人点餐:必须先点完餐,等菜做好,上菜,在此期间处理点餐的工人必须等待

后厨做菜:没啥说的,做就是了

比如工人A 处理了点餐任务,接下来它要等着 工人B 把菜做好,然后上菜,他俩也配合的蛮好

但现在同时来了两个客人,这个时候工人A 和工人B 都去处理点餐了,这时没人做饭了,饥饿

java
public class TestDeadLock {
 static final List<String> MENU = Arrays.asList("地三鲜", "宫保鸡丁", "辣子鸡丁", "烤鸡翅");
 static Random RANDOM = new Random();
 static String cooking() {
 return MENU.get(RANDOM.nextInt(MENU.size()));
 }
 public static void main(String[] args) {
 ExecutorService waiterPool = Executors.newFixedThreadPool(1);
 ExecutorService cookPool = Executors.newFixedThreadPool(1);
 waiterPool.execute(() -> {
 log.debug("处理点餐...");
 Future<String> f = cookPool.submit(() -> {
 log.debug("做菜");
 return cooking();
 });
 try {
 log.debug("上菜: {}", f.get());
 } catch (InterruptedException | ExecutionException e) {
 e.printStackTrace();
 }
 });
 waiterPool.execute(() -> {
 log.debug("处理点餐...");
 Future<String> f = cookPool.submit(() -> {
 log.debug("做菜");
 return cooking();
 });
 try {
     log.debug("上菜: {}", f.get());
 } catch (InterruptedException | ExecutionException e) {
 e.printStackTrace();
 }
 });
 }
}

终止模式之两阶段终止模式

在一个线程 T1 中如何“优雅”终止线程 T2?这里的【优雅】指的是给 T2 一个料理后事的机会。

利用 isInterrupted

interrupt 可以打断正在执行的线程,无论这个线程是在 sleep,wait,还是正常运行

java
package cn.itcast.pattern;

import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.TestTwoPhaseTermination")
public class TestTwoPhaseTermination {
    public static void main(String[] args) throws InterruptedException {
        TPTVolatile t = new TPTVolatile();
        t.start();

        Thread.sleep(3500);
        log.debug("stop");
        t.stop();
    }
}
@Slf4j(topic = "c.TPTInterrupt")
class TPTInterrupt {
    private Thread thread;

    public void start(){
        thread = new Thread(() -> {
            while(true) {
                Thread current = Thread.currentThread();
                if(current.isInterrupted()) {
                    log.debug("料理后事");
                    break;
                }
                try {
                    Thread.sleep(1000);
                    log.debug("将结果保存");
                } catch (InterruptedException e) {
                    current.interrupt();
                }

            }
        },"监控线程");
        thread.start();
    }

    public void stop() {
        thread.interrupt();
    }
}

利用停止标记

java
@Slf4j(topic = "c.TPTVolatile")
class TPTVolatile {
    private Thread thread;
    private volatile boolean stop = false;

    public void start(){
        thread = new Thread(() -> {
            while(true) {
                Thread current = Thread.currentThread();
                if(stop) {
                    log.debug("料理后事");
                    break;
                }
                try {
                    Thread.sleep(1000);
                    log.debug("将结果保存");
                } catch (InterruptedException e) {
                }
            }
        },"监控线程");
        thread.start();
    }

    public void stop() {
        stop = true;
        thread.interrupt();
    }
}

线程安全单例

单例模式有很多实现方法,饿汉、懒汉、静态内部类、枚举类,试分析每种实现下获取单例对象(即调用getInstance)时的线程安全

享元模式

当需要重用数量有限的同一类对象时

体现

包装类

在JDK中 Boolean,Byte,Short,Integer,Long,Character 等包装类提供了 valueOf 方法,例如 Long 的valueOf 会缓存 -128~127 之间的 Long 对象,在这个范围之间会重用对象,大于这个范围,才会新建 Long 对象

Byte, Short, Long 缓存的范围都是 -128~127

Character 缓存的范围是 0~127

Integer的默认范围是 -128~127

最小值不能变

但最大值可以通过调整虚拟机参数 -Djava.lang.Integer.IntegerCache.high 来改变

Boolean 缓存了 TRUE 和 FALSE

String 串池

BigDecimal BigInteger

自定义添加

如数据库连接池中的数据库连接。

六、应用

效率

使用多线程充分利用 CPU

限制

限制对 CPU 的使用

sleep 实现

在没有利用 cpu 来计算时,不要让 while(true) 空转浪费 cpu,这时可以使用 yield 或 sleep 来让出 cpu 的使用权给其他程序

java
while(true) {
 try {
 Thread.sleep(50);
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
}

可以用 wait 或 条件变量达到类似的效果

不同的是,后两种都需要加锁,并且需要相应的唤醒操作,一般适用于要进行同步的场景

sleep 适用于无需锁同步的场景

wait 实现
java
synchronized(锁对象) {
 while(条件不满足) { 
 try {
 锁对象.wait();
 } catch(InterruptedException e) {
 e.printStackTrace();
 }
 }
 // do sth...
}
条件变量实现
lock.lock();
try {
 while(条件不满足) {
 try {
 条件变量.await();
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
 }
 // do sth...
} finally {
 lock.unlock();
}

限制对共享资源的使用

semaphore 实现

使用 Semaphore 限流,在访问高峰期时,让请求线程阻塞,高峰期过去再释放许可,当然它只适合限制单机线程数量,并且仅是限制线程数,而不是限制资源数(例如连接数,请对比 Tomcat LimitLatch 的实现)

用 Semaphore 实现简单连接池,对比『享元模式』下的实现(用wait notify),性能和可读性显然更好,注意下面的实现中线程数和数据库连接数是相等的

java
package cn.itcast.n7;


import lombok.extern.slf4j.Slf4j;

import java.sql.*;
import java.util.Map;
import java.util.Properties;
import java.util.Random;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicIntegerArray;

public class Test3 {
    public static void main(String[] args) {
        Pool pool = new Pool(2);
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                Connection conn = pool.borrow();
                try {
                    Thread.sleep(new Random().nextInt(1000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                pool.free(conn);
            }).start();
        }
    }
}

@Slf4j(topic = "c.Pool")
class Pool {
    // 1. 连接池大小
    private final int poolSize;

    // 2. 连接对象数组
    private Connection[] connections;

    // 3. 连接状态数组 0 表示空闲, 1 表示繁忙
    private AtomicIntegerArray states;

    // 4. 构造方法初始化
    public Pool(int poolSize) {
        this.poolSize = poolSize;
        this.connections = new Connection[poolSize];
        this.states = new AtomicIntegerArray(new int[poolSize]);
        for (int i = 0; i < poolSize; i++) {
            connections[i] = new MockConnection("连接" + (i+1));
        }
    }

    // 5. 借连接
    public Connection borrow() {
        while(true) {
            for (int i = 0; i < poolSize; i++) {
                // 获取空闲连接
                if(states.get(i) == 0) {
                    if (states.compareAndSet(i, 0, 1)) {
                        log.debug("borrow {}", connections[i]);
                        return connections[i];
                    }
                }
            }
            // 如果没有空闲连接,当前线程进入等待
            synchronized (this) {
                try {
                    log.debug("wait...");
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    // 6. 归还连接
    public void free(Connection conn) {
        for (int i = 0; i < poolSize; i++) {
            if (connections[i] == conn) {
                states.set(i, 0);
                synchronized (this) {
                    log.debug("free {}", conn);
                    this.notifyAll();
                }
                break;
            }
        }
    }
}

class MockConnection implements Connection {

    private String name;

    public MockConnection(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "MockConnection{" +
                "name='" + name + '\'' +
                '}';
    }

    @Override
    public Statement createStatement() throws SQLException {
        return null;
    }

    @Override
    public PreparedStatement prepareStatement(String sql) throws SQLException {
        return null;
    }

    @Override
    public CallableStatement prepareCall(String sql) throws SQLException {
        return null;
    }

    @Override
    public String nativeSQL(String sql) throws SQLException {
        return null;
    }

    @Override
    public void setAutoCommit(boolean autoCommit) throws SQLException {

    }

    @Override
    public boolean getAutoCommit() throws SQLException {
        return false;
    }

    @Override
    public void commit() throws SQLException {

    }

    @Override
    public void rollback() throws SQLException {

    }

    @Override
    public void close() throws SQLException {

    }

    @Override
    public boolean isClosed() throws SQLException {
        return false;
    }

    @Override
    public DatabaseMetaData getMetaData() throws SQLException {
        return null;
    }

    @Override
    public void setReadOnly(boolean readOnly) throws SQLException {

    }

    @Override
    public boolean isReadOnly() throws SQLException {
        return false;
    }

    @Override
    public void setCatalog(String catalog) throws SQLException {

    }

    @Override
    public String getCatalog() throws SQLException {
        return null;
    }

    @Override
    public void setTransactionIsolation(int level) throws SQLException {

    }

    @Override
    public int getTransactionIsolation() throws SQLException {
        return 0;
    }

    @Override
    public SQLWarning getWarnings() throws SQLException {
        return null;
    }

    @Override
    public void clearWarnings() throws SQLException {

    }

    @Override
    public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException {
        return null;
    }

    @Override
    public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException {
        return null;
    }

    @Override
    public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException {
        return null;
    }

    @Override
    public Map<String, Class<?>> getTypeMap() throws SQLException {
        return null;
    }

    @Override
    public void setTypeMap(Map<String, Class<?>> map) throws SQLException {

    }

    @Override
    public void setHoldability(int holdability) throws SQLException {

    }

    @Override
    public int getHoldability() throws SQLException {
        return 0;
    }

    @Override
    public Savepoint setSavepoint() throws SQLException {
        return null;
    }

    @Override
    public Savepoint setSavepoint(String name) throws SQLException {
        return null;
    }

    @Override
    public void rollback(Savepoint savepoint) throws SQLException {

    }

    @Override
    public void releaseSavepoint(Savepoint savepoint) throws SQLException {

    }

    @Override
    public Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException {
        return null;
    }

    @Override
    public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException {
        return null;
    }

    @Override
    public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException {
        return null;
    }

    @Override
    public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException {
        return null;
    }

    @Override
    public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException {
        return null;
    }

    @Override
    public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException {
        return null;
    }

    @Override
    public Clob createClob() throws SQLException {
        return null;
    }

    @Override
    public Blob createBlob() throws SQLException {
        return null;
    }

    @Override
    public NClob createNClob() throws SQLException {
        return null;
    }

    @Override
    public SQLXML createSQLXML() throws SQLException {
        return null;
    }

    @Override
    public boolean isValid(int timeout) throws SQLException {
        return false;
    }

    @Override
    public void setClientInfo(String name, String value) throws SQLClientInfoException {

    }

    @Override
    public void setClientInfo(Properties properties) throws SQLClientInfoException {

    }

    @Override
    public String getClientInfo(String name) throws SQLException {
        return null;
    }

    @Override
    public Properties getClientInfo() throws SQLException {
        return null;
    }

    @Override
    public Array createArrayOf(String typeName, Object[] elements) throws SQLException {
        return null;
    }

    @Override
    public Struct createStruct(String typeName, Object[] attributes) throws SQLException {
        return null;
    }

    @Override
    public void setSchema(String schema) throws SQLException {

    }

    @Override
    public String getSchema() throws SQLException {
        return null;
    }

    @Override
    public void abort(Executor executor) throws SQLException {

    }

    @Override
    public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException {

    }

    @Override
    public int getNetworkTimeout() throws SQLException {
        return 0;
    }

    @Override
    public <T> T unwrap(Class<T> iface) throws SQLException {
        return null;
    }

    @Override
    public boolean isWrapperFor(Class<?> iface) throws SQLException {
        return false;
    }
}
单位时间内限流
guava 实现
java
@RestController
public class TestController {
 private RateLimiter limiter = RateLimiter.create(50);
 @GetMapping("/test")
 public String test() {
// limiter.acquire();
 return "ok";
 }
}

互斥

悲观互斥

互斥实际是悲观锁的思想

java

class AccountUnsafe implements Account {

    private Integer balance;

    public AccountUnsafe(Integer balance) {
        this.balance = balance;
    }

    @Override
    public Integer getBalance() {
        synchronized (this) {
            return this.balance;
        }
    }

    @Override
    public void withdraw(Integer amount) {
        synchronized (this) {
            this.balance -= amount;
        }
    }
}

interface Account {
    // 获取余额
    Integer getBalance();

    // 取款
    void withdraw(Integer amount);

    /**
     * 方法内会启动 1000 个线程,每个线程做 -10 元 的操作
     * 如果初始余额为 10000 那么正确的结果应当是 0
     */
    static void demo(Account account) {
        List<Thread> ts = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            ts.add(new Thread(() -> {
                account.withdraw(10);
            }));
        }
        long start = System.nanoTime();
        ts.forEach(Thread::start);
        ts.forEach(t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        long end = System.nanoTime();
        System.out.println(account.getBalance()
                + " cost: " + (end-start)/1000_000 + " ms");
    }
}

乐观重试

java

public class TestAccount {
    public static void main(String[] args) {
        Account account = new AccountCas(10000);
        Account.demo(account);
    }
}

class AccountCas implements Account {
    private AtomicInteger balance;

    public AccountCas(int balance) {
        this.balance = new AtomicInteger(balance);
    }

    @Override
    public Integer getBalance() {
        return balance.get();
    }

    @Override
    public void withdraw(Integer amount) {
        /*while(true) {
            // 获取余额的最新值
            int prev = balance.get();
            // 要修改的余额
            int next = prev - amount;
            // 真正修改
            if(balance.compareAndSet(prev, next)) {
                break;
            }
        }*/
        balance.getAndAdd(-1 * amount);
    }
}

class AccountUnsafe implements Account {

    private Integer balance;

    public AccountUnsafe(Integer balance) {
        this.balance = balance;
    }

    @Override
    public Integer getBalance() {
        synchronized (this) {
            return this.balance;
        }
    }

    @Override
    public void withdraw(Integer amount) {
        synchronized (this) {
            this.balance -= amount;
        }
    }
}

同步和异步

需要等待结果

既可以使用同步处理,也可以使用异步来处理

join 实现(同步)
java
static int result = 0;
private static void test1() throws InterruptedException {
 log.debug("开始");
 Thread t1 = new Thread(() -> {
 log.debug("开始");
 sleep(1);
 log.debug("结束");
 result = 10;
 }, "t1");
 t1.start();
 t1.join();
 log.debug("结果为:{}", result);
}

需要外部共享变量,不符合面向对象封装的思想

必须等待线程结束,不能配合线程池使用

Future 实现(同步)
java
private static void test2() throws InterruptedException, ExecutionException {
 log.debug("开始");
 FutureTask<Integer> result = new FutureTask<>(() -> {
 log.debug("开始");
 sleep(1);
 log.debug("结束");
 return 10;
 });
 new Thread(result, "t1").start();
 log.debug("结果为:{}", result.get());
}

规避了使用 join 之前的缺点

可以方便配合线程池使用

java
private static void test3() throws InterruptedException, ExecutionException {
 ExecutorService service = Executors.newFixedThreadPool(1);
 log.debug("开始");
 Future<Integer> result = service.submit(() -> {
 log.debug("开始");
 sleep(1);
 log.debug("结束");
 return 10;
 });
 log.debug("结果为:{}, result 的类型:{}", result.get(), result.getClass());
 service.shutdown();
}

仍然是 main 线程接收结果

get 方法是让调用线程同步等待

自定义实现(同步)

保护性暂停模式

CompletableFuture 实现(异步)
java
private static void test4() {
 // 进行计算的线程池
 ExecutorService computeService = Executors.newFixedThreadPool(1);
 // 接收结果的线程池
 ExecutorService resultService = Executors.newFixedThreadPool(1);
 log.debug("开始");
 CompletableFuture.supplyAsync(() -> {
 log.debug("开始");
 sleep(1);
 log.debug("结束");
 return 10;
 }, computeService).thenAcceptAsync((result) -> {
 log.debug("结果为:{}", result);
 }, resultService);
}

可以让调用线程异步处理结果,实际是其他线程去同步等待

可以方便地分离不同职责的线程池

以任务为中心,而不是以线程为中心

BlockingQueue 实现(异步)
private static void test6() {
 ExecutorService consumer = Executors.newFixedThreadPool(1);
 ExecutorService producer = Executors.newFixedThreadPool(1);
 BlockingQueue<Integer> queue = new SynchronousQueue<>();
 log.debug("开始");
 producer.submit(() -> {
 log.debug("开始");
 sleep(1);
 log.debug("结束");
 try {
 queue.put(10);
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
 });
 consumer.submit(() -> {
 try {
 Integer result = queue.take();
 log.debug("结果为:{}", result);
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
 });
}

不需等待结果

最好是使用异步来处理

普通线程实现
java
@Slf4j(topic = "c.FileReader")
public class FileReader {
 public static void read(String filename) {
 int idx = filename.lastIndexOf(File.separator);
 String shortName = filename.substring(idx + 1);
 try (FileInputStream in = new FileInputStream(filename)) {
 long start = System.currentTimeMillis();
 log.debug("read [{}] start ...", shortName);
 byte[] buf = new byte[1024];
 int n = -1;
 do {
 n = in.read(buf);
 } while (n != -1);
 long end = System.currentTimeMillis();
 log.debug("read [{}] end ... cost: {} ms", shortName, end - start);
 } catch (IOException e) {
 e.printStackTrace();
 }
 }
}

没有用线程时,方法的调用是同步的:

java
@Slf4j(topic = "c.Sync")
public class Sync {
 public static void main(String[] args) {
 String fullPath = "E:\\1.mp4";
 FileReader.read(fullPath);
 log.debug("do other things ...");
 }
}

使用了线程后,方法的调用时异步的:

java
private static void test1() {
 new Thread(() -> FileReader.read(Constants.MP4_FULL_PATH)).start();
 log.debug("do other things ...");
}
线程池实现
java
private static void test2() {
 ExecutorService service = Executors.newFixedThreadPool(1);
 service.execute(() -> FileReader.read(Constants.MP4_FULL_PATH));
 log.debug("do other things ...");
 service.shutdown();
}
CompletableFuture 实现
java
private static void test3() throws IOException {
 CompletableFuture.runAsync(() -> FileReader.read(Constants.MP4_FULL_PATH));
 log.debug("do other things ...");
 System.in.read();
}

缓存

缓存更新策略

更新时,是先清缓存还是先更新数据库

读写锁实现一致性缓存

使用读写锁实现一个简单的按需加载缓存

java
class GenericCachedDao<T> {
 // HashMap 作为缓存非线程安全, 需要保护
 HashMap<SqlPair, T> map = new HashMap<>();
 
 ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); 
 GenericDao genericDao = new GenericDao();
 public int update(String sql, Object... params) {
 SqlPair key = new SqlPair(sql, params);
 // 加写锁, 防止其它线程对缓存读取和更改
     lock.writeLock().lock();
 try {
 int rows = genericDao.update(sql, params);
 map.clear();
 return rows;
 } finally {
 lock.writeLock().unlock();
 }
 }
 public T queryOne(Class<T> beanClass, String sql, Object... params) {
 SqlPair key = new SqlPair(sql, params);
 // 加读锁, 防止其它线程对缓存更改
 lock.readLock().lock();
 try {
 T value = map.get(key);
 if (value != null) {
 return value;
 }
 } finally {
 lock.readLock().unlock();
 }
 // 加写锁, 防止其它线程对缓存读取和更改
 lock.writeLock().lock();
 try {
 // get 方法上面部分是可能多个线程进来的, 可能已经向缓存填充了数据
 // 为防止重复查询数据库, 再次验证
 T value = map.get(key);
 if (value == null) {
 // 如果没有, 查询数据库
 value = genericDao.queryOne(beanClass, sql, params);
 map.put(key, value);
 }
 return value;
 } finally {
 lock.writeLock().unlock();
 }
 }
 // 作为 key 保证其是不可变的
 class SqlPair {
 private String sql;
 private Object[] params;
 public SqlPair(String sql, Object[] params) {
 this.sql = sql;
 this.params = params;
 }
 @Override
 public boolean equals(Object o) {
 if (this == o) {
     return true;
 }
 if (o == null || getClass() != o.getClass()) {
 return false;
 }
 SqlPair sqlPair = (SqlPair) o;
 return sql.equals(sqlPair.sql) &&
 Arrays.equals(params, sqlPair.params);
 }
 @Override
 public int hashCode() {
 int result = Objects.hash(sql);
 result = 31 * result + Arrays.hashCode(params);
 return result;
 }
 }
}

以上实现体现的是读写锁的应用,保证缓存和数据库的一致性,但有下面的问题没有考虑

适合读多写少,如果写操作比较频繁,以上实现性能低

没有考虑缓存容量

没有考虑缓存过期

只适合单机

并发性还是低,目前只会用一把锁

更新方法太过简单粗暴,清空了所有 key(考虑按类型分区或重新设计 key)

乐观锁实现:用 CAS 去更新

分治

案例 - 单词计数

java
private static <V> void demo(Supplier<Map<String, V>> supplier, BiConsumer<Map<String, V>, 
List<String>> consumer) {
 Map<String, V> counterMap = supplier.get();
 List<Thread> ts = new ArrayList<>();
 for (int i = 1; i <= 26; i++) {
 int idx = i;
 Thread thread = new Thread(() -> {
 List<String> words = readFromFile(idx);
 consumer.accept(counterMap, words);
 });
 ts.add(thread);
 }
 ts.forEach(t -> t.start());
 ts.forEach(t -> {
 try {
     t.join();
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
 });
 System.out.println(counterMap);
}
public static List<String> readFromFile(int i) {
 ArrayList<String> words = new ArrayList<>();
 try (BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream("tmp/"
+ i + ".txt")))) {
 while (true) {
 String word = in.readLine();
 if (word == null) {
 break;
 }
 words.add(word);
 }
 return words;
 } catch (IOException e) {
 throw new RuntimeException(e);
 }
}

解法1:

java
demo(
 () -> new ConcurrentHashMap<String, LongAdder>(),
 (map, words) -> {
 for (String word : words) {
 map.computeIfAbsent(word, (key) -> new LongAdder()).increment();
 }
 }
);

解法2:

java
Map<String, Integer> collect = IntStream.range(1, 27).parallel()
 .mapToObj(idx -> readFromFile(idx))
 .flatMap(list -> list.stream())
 .collect(Collectors.groupingBy(Function.identity(), Collectors.summingInt(w -> 1)));

案例 - 求和

java

@Slf4j(topic = "c.AddTask")
class AddTask3 extends RecursiveTask<Integer> {

    int begin;
    int end;

    public AddTask3(int begin, int end) {
        this.begin = begin;
        this.end = end;
    }

    @Override
    public String toString() {
        return "{" + begin + "," + end + '}';
    }

    @Override
    protected Integer compute() {
        if (begin == end) {
            log.debug("join() {}", begin);
            return begin;
        }
        if (end - begin == 1) {
            log.debug("join() {} + {} = {}", begin, end, end + begin);
            return end + begin;
        }
        int mid = (end + begin) / 2;

        AddTask3 t1 = new AddTask3(begin, mid);
        t1.fork();
        AddTask3 t2 = new AddTask3(mid + 1, end);
        t2.fork();
        log.debug("fork() {} + {} = ?", t1, t2);

        int result = t1.join() + t2.join();
        log.debug("join() {} + {} = {}", t1, t2, result);
        return result;
    }
}

然后提交给 ForkJoinPool 来执行

java
public static void main(String[] args) {
 ForkJoinPool pool = new ForkJoinPool(4);
 System.out.println(pool.invoke(new AddTask3(1, 10)));
}

统筹

案例 - 烧水泡茶

解法1:join
java
Thread t1 = new Thread(() -> {
 log.debug("洗水壶");
 sleep(1);
 log.debug("烧开水");
 sleep(15);
}, "老王");
Thread t2 = new Thread(() -> {
 log.debug("洗茶壶");
 sleep(1);
 log.debug("洗茶杯");
 sleep(2);
 log.debug("拿茶叶");
 sleep(1);
 try {
 t1.join();
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
 log.debug("泡茶");
}, "小王");
t1.start();
t2.start();

上面模拟的是小王等老王的水烧开了,小王泡茶,如果反过来要实现老王等小王的茶叶拿来了,老王泡茶呢?代码最好能适应两种情况

上面的两个线程其实是各执行各的,如果要模拟老王把水壶交给小王泡茶,或模拟小王把茶叶交给老王泡茶呢

解法2:wait/notify
java
class S2 {
 static String kettle = "冷水";
 static String tea = null;
 static final Object lock = new Object();
 static boolean maked = false;
 public static void makeTea() {
 new Thread(() -> {
 log.debug("洗水壶");
 sleep(1);
 log.debug("烧开水");
 sleep(5);
 synchronized (lock) {
 kettle = "开水";
 lock.notifyAll();
 while (tea == null) {
 try {
 lock.wait();
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
 }
 if (!maked) {
 log.debug("拿({})泡({})", kettle, tea);
 maked = true;
 }
 }
 }, "老王").start();
 new Thread(() -> {
 log.debug("洗茶壶");
 sleep(1);
 log.debug("洗茶杯");
 sleep(2);
 log.debug("拿茶叶");
 sleep(1);
 synchronized (lock) {
 tea = "花茶";
 lock.notifyAll();
 while (kettle.equals("冷水")) {
 try {
 lock.wait();
 } catch (InterruptedException e) {
 e.printStackTrace();
     }
 }
 if (!maked) {
 log.debug("拿({})泡({})", kettle, tea);
 maked = true;
 }
 }
 }, "小王").start();
 }
}
解法3:第三者协调
java
class S3 {
 static String kettle = "冷水";
 static String tea = null;
 static final Object lock = new Object();
 public static void makeTea() {
 new Thread(() -> {
 log.debug("洗水壶");
 sleep(1);
 log.debug("烧开水");
 sleep(5);
 synchronized (lock) {
 kettle = "开水";
 lock.notifyAll();
 }
 }, "老王").start();
 new Thread(() -> {
 log.debug("洗茶壶");
 sleep(1);
 log.debug("洗茶杯");
 sleep(2);
 log.debug("拿茶叶");
 sleep(1);
 synchronized (lock) {
 tea = "花茶";
 lock.notifyAll();
 }
     }, "小王").start();
 new Thread(() -> {
 synchronized (lock) {
 while (kettle.equals("冷水") || tea == null) {
 try {
 lock.wait();
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
 }
 log.debug("拿({})泡({})", kettle, tea);
 }
 }, "王夫人").start();
 }
}

定时

定期执行

java
// 获得当前时间
LocalDateTime now = LocalDateTime.now();
// 获取本周四 18:00:00.000
LocalDateTime thursday = 
now.with(DayOfWeek.THURSDAY).withHour(18).withMinute(0).withSecond(0).withNano(0);
// 如果当前时间已经超过 本周四 18:00:00.000, 那么找下周四 18:00:00.000
if(now.compareTo(thursday) >= 0) {
 thursday = thursday.plusWeeks(1);
}
// 计算时间差,即延时执行时间
long initialDelay = Duration.between(now, thursday).toMillis();
// 计算间隔时间,即 1 周的毫秒值
long oneWeek = 7 * 24 * 3600 * 1000;
ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
System.out.println("开始时间:" + new Date());
executor.scheduleAtFixedRate(() -> {
 System.out.println("执行时间:" + new Date());
}, initialDelay, oneWeek, TimeUnit.MILLISECONDS);

七、原理

指令级并行原理

名词

Clock Cycle Time

CPU 的 Clock Cycle Time(时钟周期时间),等于主频的倒数,意思是 CPU 能够识别的最小时间单位,比如说 4G 主频的 CPU 的 Clock Cycle Time 就是 0.25 ns,作为对比,我们墙上挂钟的Cycle Time 是 1s 运行一条加法指令一般需要一个时钟周期时间

CPI

CPI (Cycles Per Instruction)指令平均时钟周期数

IPC

IPC(Instruction Per Clock Cycle) 即 CPI 的倒数,表示每个时钟周期能够运行的指令数

CPU执行时间

程序的 CPU 执行时间,即我们前面提到的 user + system 时间

程序 CPU 执行时间 = 指令数 * CPI * Clock Cycle Time

指令重排序优化

现代处理器会设计为一个时钟周期完成一条执行时间最长的 CPU 指令。

每条指令都可以分为: 取指令 - 指令译码 - 执行指令 - 内存访问 - 数据写回 这 5 个阶段

在不改变程序结果的前提下,这些指令的各个阶段可以通过重排序和组合来实现指令级并行,这一技术在 80's 中叶到 90's 中叶占据了计算架构的重要地位。

指令重排的前提是,重排指令不能影响结果

支持流水线的处理器

现代 CPU 支持多级指令流水线,例如支持同时执行 取指令 - 指令译码 - 执行指令 - 内存访问 - 数据写回 的处理器,就可以称之为五级指令流水线。这时 CPU 可以在一个时钟周期内,同时运行五条指令的不同阶段(相当于一条执行时间最长的复杂指令),IPC = 1,本质上,流水线技术并不能缩短单条指令的执行时间,但它变相地提高了指令地吞吐率。

SuperScalar 处理器

大多数处理器包含多个执行单元,并不是所有计算功能都集中在一起,可以再细分为整数运算单元、浮点数运算单元等,这样可以把多条指令也可以做到并行获取、译码等,CPU 可以在一个时钟周期内,执行多于一条指令,IPC>1

CPU 缓存结构原理

CPU 缓存结构

C:/Users/Echovin/Desktop/cpu%E7%BC%93%E5%AD%98%E7%BB%93%E6%9E%84.png

sh
# 查看 cpu 缓存
lscpu

# 查看 cpu 缓存行
cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size
64

# cpu 拿到的内存地址格式是这样的
[高位组标记][低位索引][偏移量]

速度比较

从 cpu 到大约需要的时钟周期
寄存器1 cycle
L13~4 cycle
L210~20 cycle
L340~45 cycle
内存120~240 cycle

CPU 缓存一致性

MESI 协议

  1. E、S、M 状态的缓存行都可以满足 CPU 的读请求
  2. E 状态的缓存行,有写请求,会将状态改为 M,这时并不触发向主存的写
  3. E 状态的缓存行,必须监听该缓存行的读操作,如果有,要变为 S 状态
  4. M 状态的缓存行,必须监听该缓存行的读操作,如果有,先将其它缓存(S 状态)中该缓存行变成 I 状态(即6.的流程),写入主存,自己变为 S 状态
  5. S 状态的缓存行,有写请求,走 4. 的流程
  6. S 状态的缓存行,必须监听该缓存行的失效操作,如果有,自己变为 I 状态
  7. I 状态的缓存行,有读请求,必须从主存读取

内存屏障

Memory Barrier(Memory Fence)

可见性 写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中 而读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据 有序性 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前

volatile 原理

volatile 的底层实现原理是内存屏障,Memory Barrier(Memory Fence)

对 volatile 变量的写指令后会加入写屏障 对 volatile 变量的读指令前会加入读屏障

保证可见性

写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中

java
public void actor2(I_Result r) {
 num = 2;
 ready = true; // ready 是 volatile 赋值带写屏障
 // 写屏障
}

而读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据

java
public void actor1(I_Result r) {
 // 读屏障
 // ready 是 volatile 读取值带读屏障
 if(ready) {
 r.r1 = num + num;
 } else {
 r.r1 = 1;
 }
}

保证有序性

写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后

java
public void actor2(I_Result r) {
 num = 2;
 ready = true; // ready 是 volatile 赋值带写屏障
 // 写屏障
}

读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前

java
public void actor1(I_Result r) {
 // 读屏障
 // ready 是 volatile 读取值带读屏障
 if(ready) {
 r.r1 = num + num;
 } else {
 r.r1 = 1;
 }
}

写屏障仅仅是保证之后的读能够读到最新的结果,但不能保证读跑到它前面去 而有序性的保证也只是保证了本线程内相关代码不被重排序

double-checked locking 问题

java
public final class Singleton {
 private Singleton() { }
 private static Singleton INSTANCE = null;
 public static Singleton getInstance() { 
 if(INSTANCE == null) { // t2
 // 首次访问会同步,而之后的使用没有 synchronized
 synchronized(Singleton.class) {
 if (INSTANCE == null) { // t1
 INSTANCE = new Singleton();
 } 
 }
 }
 return INSTANCE;
 }
}

懒惰实例化 首次使用 getInstance() 才使用 synchronized 加锁,后续使用时无需加锁 有隐含的,但很关键的一点:第一个 if 使用了 INSTANCE 变量,是在同步块之外

但在多线程环境下,上面的代码是有问题的

关键在于 0: getstatic 这行代码在 monitor 控制之外,它就像之前举例中不守规则的人,可以越过 monitor 读取INSTANCE 变量的值 这时 t1 还未完全将构造方法执行完毕,如果在构造方法中要执行很多初始化操作,那么 t2 拿到的是将是一个未初始化完毕的单例 对 INSTANCE 使用 volatile 修饰即可,可以禁用指令重排,但要注意在 JDK 5 以上的版本的 volatile 才会真正有效

double-checked locking 解决

java
public final class Singleton {
 private Singleton() { }
 private static volatile Singleton INSTANCE = null;
 public static Singleton getInstance() {
 // 实例没创建,才会进入内部的 synchronized代码块
 if (INSTANCE == null) { 
 synchronized (Singleton.class) { // t2
 // 也许有其它线程已经创建实例,所以再判断一次
 if (INSTANCE == null) { // t1
 INSTANCE = new Singleton();
 }
 }
 }
 return INSTANCE;
 }
}

读写 volatile 变量时会加入内存屏障(Memory Barrier(Memory Fence))

可见性 写屏障(sfence)保证在该屏障之前的 t1 对共享变量的改动,都同步到主存当中 而读屏障(lfence)保证在该屏障之后 t2 对共享变量的读取,加载的是主存中最新数据 有序性 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前

更底层是读写变量时使用 lock 指令来多核 CPU 之间的可见性与有序性

final 原理

设置 final 变量的原理

final 变量的赋值也会通过 putfield 指令来完成,同样在这条指令之后也会加入写屏障,保证在其它线程读到它的值时不会出现为 0 的情况

获取 final 变量的原理

Monitor 原理

Monitor 被翻译为监视器或管程 每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针

刚开始 Monitor 中 Owner 为 null 当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor中只能有一个 Owner 在 Thread-2 上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行 synchronized(obj),就会进入EntryList BLOCKED Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争的时是非公平的 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程

synchronized 必须是进入同一个对象的 monitor 才有上述的效果 不加 synchronized 的对象不会关联监视器,不遵从以上规则

synchronized 原理